jamiepine/voicebox · error · FileNotFoundError

Audio for capture {capture_id} is missing

Error message

Audio for capture {capture_id} is missing

What it means

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.

Source

Thrown at backend/services/captures.py:220

    row.refinement_flags = json.dumps(flags.to_dict())
    db.commit()
    db.refresh(row)
    return _to_response(row)


async def retranscribe_capture(
    capture_id: str,
    stt_model: Optional[str],
    language: Optional[str],
    db: Session,
) -> Optional[CaptureResponse]:
    row = db.query(DBCapture).filter(DBCapture.id == capture_id).first()
    if not row:
        return None

    resolved = config.resolve_storage_path(row.audio_path)
    if not resolved or not resolved.exists():
        raise FileNotFoundError(f"Audio for capture {capture_id} is missing")

    whisper = get_whisper_model()
    resolved_stt = stt_model or whisper.model_size
    transcript = await whisper.transcribe(str(resolved), language, resolved_stt)

    row.transcript_raw = transcript
    row.stt_model = resolved_stt
    if language:
        row.language = language
    # Refined text is stale after a fresh STT pass — force a re-refine.
    row.transcript_refined = None
    row.llm_model = None
    row.refinement_flags = None
    db.commit()
    db.refresh(row)
    return _to_response(row)

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Confirm the configured storage roots include the capture audio location and the file is present on disk.
  2. If the asset is permanently lost, delete the capture row (DELETE /captures/{id}) rather than retrying retranscribe.
  3. Treat HTTP 410 from this endpoint as 'asset gone' — re-recording is the recovery, not a retry.
  4. Audit audio_path values for absolute paths that break after a data-dir move.

Example fix

# before
resolved = config.resolve_storage_path(row.audio_path)
if not resolved or not resolved.exists():
    raise FileNotFoundError(f"Audio for capture {capture_id} is missing")

# after (record the loss on the row so list views can surface it)
resolved = config.resolve_storage_path(row.audio_path)
if not resolved or not resolved.exists():
    row.audio_missing = True
    db.commit()
    raise FileNotFoundError(f"Audio for capture {capture_id} is missing")
Defensive patterns

Strategy: try-catch

Validate before calling

async function retranscribeOnlyIfAudioExists(api, captureId) {
  const r = await api.getCaptureAudio(captureId, { method: 'HEAD' });
  if (!r.ok) throw new Error('capture audio is missing');
  return api.retranscribeCapture(captureId, payload);
}

Type guard

function captureHasAudioRow(capture) {
  return Boolean(capture && typeof capture.audio_path === 'string' && capture.audio_path.length);
}

Try / catch

try { await api.retranscribeCapture(captureId, payload); }
catch (e) {
  if (e.status === 410) { markCaptureAudioMissing(captureId); notify('Original audio is gone — please re-record'); }
  else if (e.status === 404) { dropCapture(captureId); }
  else throw e;
}

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Related errors


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