jamiepine/voicebox · error · HTTPException
Story item not found or invalid split point
Error message
Story item not found or invalid split point
What it means
Raised (HTTP 404) by POST /stories/{story_id}/items/{item_id}/split when the underlying stories.split_story_item service returns None. The service returns None for two conflated reasons: (a) the story item, its story, or its parent generation row cannot be found, or (b) the supplied split_time_ms is out of range — split_time_ms must satisfy 0 < split_time_ms < effective_duration_ms (effective_duration_ms = generation.duration_ms - trim_start_ms - trim_end_ms). Because the route collapses both into a 404, an invalid split point looks indistinguishable from a missing item to the client.
Source
Thrown at backend/routes/stories.py:178
):
"""Set a story item's per-clip volume (linear gain, 0.0–2.0)."""
item = await stories.update_story_item_volume(story_id, item_id, data, db)
if item is None:
raise HTTPException(status_code=404, detail="Story item not found")
return item
@router.post("/stories/{story_id}/items/{item_id}/split", response_model=list[models.StoryItemDetail])
async def split_story_item(
story_id: str,
item_id: str,
data: models.StoryItemSplit,
db: Session = Depends(get_db),
):
"""Split a story item at a given time, creating two clips."""
items = await stories.split_story_item(story_id, item_id, data, db)
if items is None:
raise HTTPException(status_code=404, detail="Story item not found or invalid split point")
return items
@router.post("/stories/{story_id}/items/{item_id}/duplicate", response_model=models.StoryItemDetail)
async def duplicate_story_item(
story_id: str,
item_id: str,
db: Session = Depends(get_db),
):
"""Duplicate a story item."""
item = await stories.duplicate_story_item(story_id, item_id, db)
if item is None:
raise HTTPException(status_code=404, detail="Story item not found")
return item
@router.put("/stories/{story_id}/items/{item_id}/version", response_model=models.StoryItemDetail)
async def set_story_item_version(View on GitHub (pinned to 51f49dea19)
Solutions
- Confirm split_time_ms is strictly between 0 and effective_duration_ms using the item's trim_start_ms/trim_end_ms and generation.duration before calling.
- Verify the item still exists under story_id (GET /stories/{story_id}) before splitting — a stale item_id from a prior split/duplicate causes this.
- If splitting at the clip-relative midpoint, compute split_time_ms = effective_duration_ms // 2, never effective_duration_ms itself.
- Treat HTTP 404 here as ambiguous: re-fetch the item; if it exists, the split point was invalid (file as a 400/422 suggestion upstream).
Example fix
// before
const splitAt = item.start_time_ms + playheadMs; // wrong: absolute, not clip-relative
await api.splitItem(storyId, itemId, { split_time_ms: splitAt });
// after
const effective = item.duration_ms - (item.trim_start_ms ?? 0) - (item.trim_end_ms ?? 0);
if (playheadMs <= 0 || playheadMs >= effective) {
throw new Error(`split point must be within (0, ${effective})`);
}
await api.splitItem(storyId, itemId, { split_time_ms: playheadMs }); Defensive patterns
Strategy: validation
Validate before calling
function clampSplitPoint(item, splitTimeMs) {
const trimStart = item.trim_start_ms ?? 0;
const trimEnd = item.trim_end_ms ?? 0;
const effective = Math.round(item.duration_ms) - trimStart - trimEnd;
if (!Number.isFinite(splitTimeMs) || splitTimeMs <= 0 || splitTimeMs >= effective) {
return { ok: false, effective, reason: `split_time_ms must be within (0, ${effective})` };
}
return { ok: true, effective };
}
// call before POST .../split
const check = clampSplitPoint(item, payload.split_time_ms);
if (!check.ok) throw new Error(check.reason); Type guard
function isValidSplitPayload(item, p) {
if (!item || typeof item.duration_ms !== 'number') return false;
if (typeof p?.split_time_ms !== 'number') return false;
const effective = item.duration_ms - (item.trim_start_ms ?? 0) - (item.trim_end_ms ?? 0);
return p.split_time_ms > 0 && p.split_time_ms < effective;
} Try / catch
// none — validate pre-flight; a 404 here is ambiguous (missing item vs. bad split),
// so re-fetch the item to disambiguate rather than catching.
if (!isValidSplitPayload(item, payload)) { warnUser(); return; } Prevention
- Always compute split_time_ms relative to the trimmed clip, not the absolute playhead.
- Re-fetch the item right before splitting to avoid stale ids from concurrent edits.
- Disable the split control when effective duration is <= 1ms.
When it happens
Trigger: POST /stories/{story_id}/items/{item_id}/split with a split_time_ms of 0, a negative value, or a value >= the clip's effective (trimmed) duration. Also when item_id does not exist under the given story_id, or the item's generation_id has been deleted.
Common situations: Client computes split time from the wrong baseline (absolute timeline vs. clip-relative timeline), splits at the exact clip end, or operates on an item that was already split/deleted by a concurrent request. UI passing the raw playhead position instead of the offset into the trimmed clip.
Related errors
- Story item or version not found
- Story has no audio items
- {str(e)}
- Invalid timecode update request
- Invalid reorder request - ensure all generation IDs belong t
AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12).
Data as JSON: /api/errors/26ca688bf22d6c7c.
Report an issue: GitHub.