{"record":{"id":"3d7da069d37d20fd","repo":"jamiepine/voicebox","slug":"generation-has-no-audio-file","errorCode":null,"errorMessage":"Generation has no audio file","messagePattern":"Generation has no audio file","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"warning","filePath":"backend/routes/history.py","lineNumber":176,"sourceCode":"    return StreamingResponse(\n        io.BytesIO(zip_bytes),\n        media_type=\"application/zip\",\n        headers={\"Content-Disposition\": safe_content_disposition(\"attachment\", filename)},\n    )\n\n\n@router.get(\"/history/{generation_id}/export-audio\")\nasync def export_generation_audio(\n    generation_id: str,\n    db: Session = Depends(get_db),\n):\n    \"\"\"Export only the audio file from a generation.\"\"\"\n    generation = db.query(DBGeneration).filter_by(id=generation_id).first()\n    if not generation:\n        raise HTTPException(status_code=404, detail=\"Generation not found\")\n\n    if not generation.audio_path:\n        raise HTTPException(status_code=404, detail=\"Generation has no audio file\")\n\n    audio_path = config.resolve_storage_path(generation.audio_path)\n    if audio_path is None or not audio_path.is_file():\n        raise HTTPException(status_code=404, detail=\"Audio file not found\")\n\n    safe_text = \"\".join(c for c in generation.text[:30] if c.isalnum() or c in (\" \", \"-\", \"_\")).strip()\n    if not safe_text:\n        safe_text = \"generation\"\n    # Append a short id so exports of similarly-worded generations don't collide\n    # on the same filename (the first 30 chars are frequently identical).\n    filename = f\"{safe_text}-{generation_id[:8]}.wav\"\n\n    return FileResponse(\n        audio_path,\n        media_type=\"audio/wav\",\n        headers={\"Content-Disposition\": safe_content_disposition(\"attachment\", filename)},\n    )\n","sourceCodeStart":158,"sourceCodeEnd":194,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/history.py#L158-L194","documentation":"Returned by GET /history/{generation_id}/export-audio when the generation row exists but `generation.audio_path` is falsy (None or empty string). This means the row was created without an audio file — e.g. a failed generation that never produced output, or an imported non-audio row. HTTP 404.","triggerScenarios":"Exporting audio for a generation whose status is 'failed' (no audio written); a row created for metadata-only purposes; a generation still in progress (loading_model/generating) that hasn't written audio yet.","commonSituations":"User clicks 'download audio' on a failed or in-progress generation; legacy rows from before audio_path was populated.","solutions":["Hide/disable the audio-export action unless the generation has a non-empty audio_path and a terminal status.","On 404 'Generation has no audio file.', show a user-facing message rather than retrying.","Filter the history list so rows without audio_path can't trigger this endpoint."],"exampleFix":"// before\n<button onClick={() => downloadAudio(id)}>Download</button>\n\n// after\n<button disabled={!row.audio_path || !['completed'].includes(row.status)}\n        onClick={() => downloadAudio(id)}>Download</button>","handlingStrategy":"validation","validationCode":"if (!row.audio_path) { /* hide audio-export action */ return; }\nif (!['completed'].includes(row.status)) { /* not ready */ return; }\nawait fetch(`/history/${id}/export-audio`);","typeGuard":"function hasAudioPath(row) {\n  return row != null && typeof row.audio_path === 'string' && row.audio_path.length > 0;\n}","tryCatchPattern":"try {\n  const res = await fetch(`/history/${id}/export-audio`);\n  if (res.status === 404) {\n    const d = await res.json();\n    if (/no audio file/i.test(d.detail)) { alert('No audio for this generation'); return; }\n  }\n} catch (e) { console.error(e); }","preventionTips":["Disable the audio-export action when audio_path is empty or status isn't completed.","Don't offer the action on failed/in-progress rows."],"tags":["fastapi","not-found","history","export","audio","missing-asset","validation"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T23:17:12.415Z"}