jamiepine/voicebox · error · HTTPException

str(e)

Error message

str(e)

What it means

Raised (HTTP 500) by GET /stories/{story_id}/export-audio as the catch-all for any non-HTTPException thrown during export (the handler re-raises genuine HTTPException, then wraps everything else). The detail is the raw str(e), so the client sees whatever exception message the audio-mixing stack produced. Common underlying causes live in stories.export_story_audio: librosa/soundfile decode failures, missing generation audio files on disk, numpy shape mismatches, or sample-rate assumptions (it defaults to 24000 Hz).

Source

Thrown at backend/routes/stories.py:237

        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

  1. Check the server logs for the wrapped exception's traceback — the real cause is there, not in the 500 detail.
  2. Verify every item's generation has an on-disk audio file before exporting.
  3. Replace detail=str(e) with a generic message and log the exception server-side to avoid leaking internals.
  4. If sample-rate mismatches are the cause, ensure all generations are produced at the same rate (the service hardcodes 24000).

Example fix

# before
    except Exception as e:
        raise HTTPException(status_code=500, detail=str(e))

# after
    except Exception:
        logger.exception("export_story_audio failed for story %s", story_id)
        raise HTTPException(status_code=500, detail="Audio export failed; see server logs")
Defensive patterns

Strategy: try-catch

Validate before calling

// Pre-flight: ensure every item has a resolvable audio asset before export.
async function preflightExport(api, storyId) {
  const story = await api.getStory(storyId).then(r => r.json());
  const missing = (story.items ?? []).filter(i => !i.audio_path);
  if (missing.length) throw new Error(`${missing.length} item(s) missing audio`);
  return story;
}

Type guard

function itemsHaveAudio(story) {
  return (story.items ?? []).every(i => typeof i.audio_path === 'string' && i.generation_id);
}

Try / catch

try { await api.exportStoryAudio(storyId); }
catch (e) {
  if (e.status === 500) {
    notify('Export failed; the server log has details. Check for missing audio files.');
    // do NOT blind-retry; surface to the user
  } else throw e;
}

Prevention

When it happens

Trigger: A story item's generation audio file is missing from disk (deleted/moved data dir), an audio decode error in the mixing loop, or a malformed/zero-length audio array causing the concatenation/mixing math to throw.

Common situations: Data directory moved or partially restored from backup; a generation row points at a path that no longer exists; mismatched sample rates between generations breaking the mix; disk full during temp WAV write.

Related errors


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