jamiepine/voicebox · error · HTTPException

Audio file not found

Error message

Audio file not found

What it means

Returned as a 404 from GET /captures/{capture_id}/audio when the DBCapture row exists but the audio file is gone — config.resolve_storage_path returned None or the path does not exist (.exists() is False). The row exists but the underlying uploaded WAV is missing, so streaming is impossible. Note this endpoint uses .exists() rather than .is_file(), so a directory at that path would also fail.

Source

Thrown at backend/routes/captures.py:102

@router.get("/captures/{capture_id}", response_model=models.CaptureResponse)
async def get_capture_endpoint(capture_id: str, db: Session = Depends(get_db)):
    capture = captures_service.get_capture(capture_id, db)
    if not capture:
        raise HTTPException(status_code=404, detail="Capture not found")
    return capture


@router.get("/captures/{capture_id}/audio")
async def get_capture_audio_endpoint(capture_id: str, db: Session = Depends(get_db)):
    """Stream the original capture audio file."""
    row = db.query(DBCapture).filter(DBCapture.id == capture_id).first()
    if not row:
        raise HTTPException(status_code=404, detail="Capture not found")

    audio_path = config.resolve_storage_path(row.audio_path)
    if audio_path is None or not audio_path.exists():
        raise HTTPException(status_code=404, detail="Audio file not found")

    return FileResponse(
        audio_path,
        media_type="audio/wav",
        filename=f"capture_{capture_id}.wav",
    )


@router.delete("/captures/{capture_id}")
async def delete_capture_endpoint(capture_id: str, db: Session = Depends(get_db)):
    deleted = captures_service.delete_capture(capture_id, db)
    if not deleted:
        raise HTTPException(status_code=404, detail="Capture not found")
    return {"message": f"Capture {capture_id} deleted"}


@router.post("/captures/{capture_id}/refine", response_model=models.CaptureResponse)
async def refine_capture_endpoint(

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Verify config.resolve_storage_path(row.audio_path) returns a real path and STORAGE_DIR is correct.
  2. Check the file exists on disk; if removed, the audio is unrecoverable — re-capture.
  3. Make capture creation atomic: commit the row only after the audio write is fsynced and verified.
  4. Audit cleanup logic to delete rows together with their audio files.
Defensive patterns

Strategy: validation

Validate before calling

const cap = await getCapture(captureId);
if (!cap || !cap.audio_path) {
  warn('Capture audio may be missing on the server');
}

Type guard

function captureHasArtifact(c: { audio_path: string | null } | null): boolean {
  return c !== null && typeof c.audio_path === 'string' && c.audio_path.length > 0;
}

Try / catch

try {
  const r = await fetch(`/captures/${id}/audio`);
  if (r.status === 404 && (await r.json()).detail === 'Audio file not found') {
    markCaptureAudioCorrupt(id); // file gone, row remains
    return;
  }
  play(await r.blob());
} catch (e) { showNetworkError(e); }

Prevention

When it happens

Trigger: GET /captures/{capture_id}/audio where row.audio_path is null, unresolvable under STORAGE_DIR, or the file was deleted/moved after the capture was created.

Common situations: Capture cleanup job deleted audio files but kept rows; storage volume unmounted; STORAGE_DIR reconfigured; the create flow wrote the DB row before the audio finished flushing.

Related errors


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