{"record":{"id":"c681712ace9d1e90","repo":"jamiepine/voicebox","slug":"audio-for-capture-capture-id-is-missing","errorCode":null,"errorMessage":"Audio for capture {capture_id} is missing","messagePattern":"Audio for capture (.+?) is missing","errorType":"exception","errorClass":"FileNotFoundError","httpStatus":null,"severity":"error","filePath":"backend/services/captures.py","lineNumber":220,"sourceCode":"    row.refinement_flags = json.dumps(flags.to_dict())\n    db.commit()\n    db.refresh(row)\n    return _to_response(row)\n\n\nasync def retranscribe_capture(\n    capture_id: str,\n    stt_model: Optional[str],\n    language: Optional[str],\n    db: Session,\n) -> Optional[CaptureResponse]:\n    row = db.query(DBCapture).filter(DBCapture.id == capture_id).first()\n    if not row:\n        return None\n\n    resolved = config.resolve_storage_path(row.audio_path)\n    if not resolved or not resolved.exists():\n        raise FileNotFoundError(f\"Audio for capture {capture_id} is missing\")\n\n    whisper = get_whisper_model()\n    resolved_stt = stt_model or whisper.model_size\n    transcript = await whisper.transcribe(str(resolved), language, resolved_stt)\n\n    row.transcript_raw = transcript\n    row.stt_model = resolved_stt\n    if language:\n        row.language = language\n    # Refined text is stale after a fresh STT pass — force a re-refine.\n    row.transcript_refined = None\n    row.llm_model = None\n    row.refinement_flags = None\n    db.commit()\n    db.refresh(row)\n    return _to_response(row)\n","sourceCodeStart":202,"sourceCodeEnd":237,"githubUrl":"https://github.com/jamiepine/voicebox/blob/51f49dea198384b4eb6087b72c17057c6eb1c1cd/backend/services/captures.py#L202-L237","documentation":"Raised as FileNotFoundError (translated to HTTP 410 Gone by the /captures/{id}/retranscribe route) when the capture row exists but the audio file at row.audio_path cannot be resolved to an existing path via config.resolve_storage_path. This signals the metadata is present but the underlying asset is gone — hence 410 rather than 404. It fires before any Whisper call.","triggerScenarios":"POST /captures/{capture_id}/retranscribe for a capture whose audio file was deleted from disk (manual cleanup, partial backup restore, moved DATA_DIR), or whose audio_path points outside the configured storage roots so resolve_storage_path returns None.","commonSituations":"Data directory migrated/restored incompletely; a cleanup script removed capture audio but left DB rows; audio_path stored as an absolute path that no longer exists after a host/path rename; storage root not configured so resolution fails.","solutions":["Confirm the configured storage roots include the capture audio location and the file is present on disk.","If the asset is permanently lost, delete the capture row (DELETE /captures/{id}) rather than retrying retranscribe.","Treat HTTP 410 from this endpoint as 'asset gone' — re-recording is the recovery, not a retry.","Audit audio_path values for absolute paths that break after a data-dir move."],"exampleFix":"# before\nresolved = config.resolve_storage_path(row.audio_path)\nif not resolved or not resolved.exists():\n    raise FileNotFoundError(f\"Audio for capture {capture_id} is missing\")\n\n# after (record the loss on the row so list views can surface it)\nresolved = config.resolve_storage_path(row.audio_path)\nif not resolved or not resolved.exists():\n    row.audio_missing = True\n    db.commit()\n    raise FileNotFoundError(f\"Audio for capture {capture_id} is missing\")","handlingStrategy":"try-catch","validationCode":"async function retranscribeOnlyIfAudioExists(api, captureId) {\n  const r = await api.getCaptureAudio(captureId, { method: 'HEAD' });\n  if (!r.ok) throw new Error('capture audio is missing');\n  return api.retranscribeCapture(captureId, payload);\n}","typeGuard":"function captureHasAudioRow(capture) {\n  return Boolean(capture && typeof capture.audio_path === 'string' && capture.audio_path.length);\n}","tryCatchPattern":"try { await api.retranscribeCapture(captureId, payload); }\ncatch (e) {\n  if (e.status === 410) { markCaptureAudioMissing(captureId); notify('Original audio is gone — please re-record'); }\n  else if (e.status === 404) { dropCapture(captureId); }\n  else throw e;\n}","preventionTips":["Treat 410 from retranscribe as permanent asset loss — don't retry, re-record.","Keep capture audio files within the configured storage roots.","Audit audio_path values after any data-dir migration."],"tags":["api","captures","filesystem","data-integrity","not-found"],"backgroundTag":null,"analyzedSha":"51f49dea198384b4eb6087b72c17057c6eb1c1cd","analyzedAt":"2026-08-12T16:51:42.824Z","schemaVersion":2},"datasetVersion":"2026-08-12T18:17:37.767Z"}