jamiepine/voicebox · warning · HTTPException
Story has no audio items
Error message
Story has no audio items
What it means
Raised (HTTP 400) by GET /stories/{story_id}/export-audio when the story exists but stories.export_story_audio returns None/falsy. The service returns None when the story has no DBStoryItem rows at all, or none of its items join to a DBGeneration (so there is no audio to mix into a single file). The 400 (rather than 404) signals 'story exists but is empty'.
Source
Thrown at backend/routes/stories.py:222
if item is None:
raise HTTPException(status_code=404, detail="Story item or version not found")
return item
@router.get("/stories/{story_id}/export-audio")
async def export_story_audio(
story_id: str,
db: Session = Depends(get_db),
):
"""Export story as single mixed audio file."""
try:
story = db.query(database.Story).filter_by(id=story_id).first()
if not story:
raise HTTPException(status_code=404, detail="Story not found")
audio_bytes = await stories.export_story_audio(story_id, db)
if not audio_bytes:
raise HTTPException(status_code=400, detail="Story has no audio items")
safe_name = "".join(c for c in story.name if c.isalnum() or c in (" ", "-", "_")).strip()
if not safe_name:
safe_name = "story"
filename = f"{safe_name}.wav"
return StreamingResponse(
io.BytesIO(audio_bytes),
media_type="audio/wav",
headers={"Content-Disposition": safe_content_disposition("attachment", filename)},
)
except HTTPException:
raise
except Exception as e:
raise HTTPException(status_code=500, detail=str(e))
View on GitHub (pinned to 51f49dea19)
Solutions
- Disable the export control client-side until the story has at least one item with audio.
- If hit unexpectedly, inspect the story items and confirm each item's generation_id resolves to an existing generation.
- Treat 400 here as 'add clips first', not a server fault.
Example fix
// before
<button onClick={() => exportStory(story.id)}>Export</button>
// after
<button disabled={story.item_count === 0} onClick={() => exportStory(story.id)}>Export</button> Defensive patterns
Strategy: validation
Validate before calling
function canExport(story) {
return Boolean(story && (story.item_count ?? 0) > 0);
}
if (!canExport(story)) { notify('Add at least one clip before exporting'); return; } Type guard
function hasPlayableItems(story) {
return Array.isArray(story.items) && story.items.some(i => i.generation_id);
} Try / catch
try { await api.exportStoryAudio(storyId); }
catch (e) {
if (e.status === 400) notify('This story has no audio to export');
else throw e;
} Prevention
- Disable export until the story has at least one item with audio.
- Treat 400 from this endpoint as an empty-state, not an error to retry.
- Periodically reconcile items whose generation_id no longer exists.
When it happens
Trigger: Calling export on a freshly created story with no clips added, or a story whose items all reference deleted generations so the inner join yields zero rows.
Common situations: Export button enabled on an empty story; user deletes every generation but the story shell remains; importing a story that lost its generation references.
Related errors
- Story item not found or invalid split point
- Story item or version not found
- {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/1f09ffc0ff454590.
Report an issue: GitHub.