{"record":{"id":"7145047b81684cf3","repo":"jamiepine/voicebox","slug":"generation-failed-no-audio-available","errorCode":null,"errorMessage":"Generation failed; no audio available","messagePattern":"Generation failed; no audio available","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"backend/routes/audio.py","lineNumber":61,"sourceCode":"        filename=f\"generation_{version.generation_id}_{version.label}{audio_path.suffix}\",\n    )\n\n\n@router.get(\"/audio/{generation_id}\")\nasync def get_audio(generation_id: str, db: Session = Depends(get_db)):\n    \"\"\"Serve generated audio file (serves the default version).\"\"\"\n    generation = await history.get_generation(generation_id, db)\n    if not generation:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    audio_path = config.resolve_storage_path(generation.audio_path)\n    if audio_path is None or not audio_path.is_file():\n        detail = (\n            \"Generation failed; no audio available\"\n            if generation.status == \"failed\"\n            else \"Audio file not found\"\n        )\n        raise HTTPException(status_code=404, detail=detail)\n\n    return FileResponse(\n        audio_path,\n        media_type=_audio_media_type(audio_path),\n        filename=f\"generation_{generation_id}{audio_path.suffix}\",\n    )\n\n\n@router.get(\"/samples/{sample_id}\")\nasync def get_sample_audio(sample_id: str, db: Session = Depends(get_db)):\n    \"\"\"Serve profile sample audio file.\"\"\"\n    from ..database import ProfileSample as DBProfileSample\n\n    sample = db.query(DBProfileSample).filter_by(id=sample_id).first()\n    if not sample:\n        raise HTTPException(status_code=404, detail=\"Sample not found\")\n\n    audio_path = config.resolve_storage_path(sample.audio_path)","sourceCodeStart":43,"sourceCodeEnd":79,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/audio.py#L43-L79","documentation":"Returned as a 404 from GET /audio/{generation_id} when a generation record exists, its status is 'failed', and there is no playable audio artifact on disk (resolve_storage_path returned None or the path is not a regular file). The handler deliberately distinguishes a failed generation from a missing file so the frontend can show 'generation failed' rather than a generic 404. It is a data-state error, not a code bug.","triggerScenarios":"GET /audio/{generation_id} where the generation row has status=='failed' and generation.audio_path is null, empty, outside the configured storage root, or points to a non-existent/non-regular file. Happens after a TTS/voice-clone job that errored before writing audio.","commonSituations":"The TTS engine or model download crashed mid-generation; the worker marked status failed but never wrote the file. Or storage was migrated/cleaned and stale failed rows remain. Or config.resolve_storage_path rejects the stored relative path after a STORAGE_DIR change.","solutions":["Inspect the generation row's status and error fields: if status is 'failed', surface the upstream failure reason to the user instead of retrying the audio fetch.","Verify config.resolve_storage_path(generation.audio_path) returns a real path and that STORAGE_DIR matches where jobs actually write output.","If failed rows are stale, delete or re-run the generation so it produces a fresh audio_path.","Guard the frontend: when generation status is 'failed', show the failure UI and do not request the audio stream."],"exampleFix":"// before\nconst audio = await fetch(`/audio/${id}`); // 404 on failed gens\n// after\nconst gen = await getGeneration(id);\nif (gen.status === 'failed') {\n  showError(gen.error || 'Generation failed');\n} else {\n  const audio = await fetch(`/audio/${id}`);\n}","handlingStrategy":"validation","validationCode":"// Before requesting audio, check the generation status from the generation detail endpoint.\nconst gen = await getGeneration(generationId);\nif (gen.status === 'failed') {\n  // do not fetch /audio/{id}; show the failure instead\n  throw new Error('Generation failed; audio will not be available');\n}","typeGuard":"type GenerationState = 'pending' | 'completed' | 'failed';\nfunction isAudioAvailable(gen: { status: GenerationState; audio_path: string | null }): boolean {\n  return gen.status === 'completed' && !!gen.audio_path;\n}","tryCatchPattern":"try {\n  const r = await fetch(`/audio/${id}`);\n  if (r.status === 404) {\n    const body = await r.json();\n    if (body.detail === 'Generation failed; no audio available') {\n      showFailureUI();\n    } else {\n      showMissingFileUI();\n    }\n    return;\n  }\n  play(await r.blob());\n} catch (e) {\n  showNetworkError(e);\n}","preventionTips":["Gate the audio request on generation.status === 'completed' and a non-null audio_path.","Show the upstream failure reason in the UI when status is 'failed' instead of requesting audio.","Do not cache audio URLs for failed or pending generations."],"tags":["audio","storage","not-found","state-mismatch","fastapi"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}