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

  1. 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.
  2. 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.
  3. If splitting at the clip-relative midpoint, compute split_time_ms = effective_duration_ms // 2, never effective_duration_ms itself.
  4. 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

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


AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12). Data as JSON: /api/errors/26ca688bf22d6c7c. Report an issue: GitHub.