{"record":{"id":"6405f111224e64ca","repo":"jamiepine/voicebox","slug":"audio-file-not-found-6405f1","errorCode":null,"errorMessage":"Audio file not found","messagePattern":"Audio file not found","errorType":"http","errorClass":"HTTPException","httpStatus":404,"severity":"error","filePath":"backend/routes/history.py","lineNumber":180,"sourceCode":"    )\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":162,"sourceCodeEnd":194,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/routes/history.py#L162-L194","documentation":"Returned by GET /history/{generation_id}/export-audio when `config.resolve_storage_path(generation.audio_path)` returns None OR the resolved path does not satisfy `.is_file()`. This is the case where the DB row points at an audio file, but the file is absent from disk — an orphaned reference. HTTP 404.","triggerScenarios":"The audio file was deleted/moved from the generations directory while the DB row remains; the storage path config changed so resolve_storage_path can no longer map the stored relative path; the file is on an unmounted external volume.","commonSituations":"Manual cleanup of the generations dir; migrations that moved storage without updating rows; relative-path mismatch after the configured base dir changed; unmounted network storage.","solutions":["Verify config.get_generations_dir() / the storage root points at the same volume the rows were written to.","If audio was lost, delete the orphaned generation row or re-generate it.","Run a consistency sweep: for each row with audio_path, confirm the file exists; clean orphans.","Keep the storage root stable across app updates to avoid path resolution failures."],"exampleFix":"// before: assume file exists\nwindow.open(`/history/${id}/export-audio`);\n\n// after: health-check storage on startup, surface orphan rows\nfor (const row of history) {\n  if (row.audio_path && !await fileExists(row.audio_path)) flagOrphan(row.id);\n}","handlingStrategy":"validation","validationCode":"// Client cannot check disk; guard on status+path, and run server-side consistency checks\nif (!hasAudioPath(row)) return;\n// Server-side periodic job:\n// for row in generations: assert resolve_storage_path(row.audio_path).is_file()","typeGuard":"function pointsToExistingAudioSync(row, existsFn) {\n  return row != null && typeof row.audio_path === 'string' && existsFn(row.audio_path);\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 (/audio file not found/i.test(d.detail)) { flagOrphan(id); return; }\n  }\n} catch (e) { console.error(e); }","preventionTips":["Keep the storage root path stable across updates.","Run a consistency sweep to detect orphaned audio_path references.","Never manually delete files from the generations directory without updating rows."],"tags":["fastapi","not-found","history","export","audio","missing-asset","storage","orphan","filesystem"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}