jamiepine/voicebox · warning · ValueError

Invalid source '{source}'. Must be one of {sorted(VALID_SOUR

Error message

Invalid source '{source}'. Must be one of {sorted(VALID_SOURCES)}

What it means

Raised as ValueError (translated to HTTP 400 by the /captures route) when create_capture is called with a source not in VALID_SOURCES = {'dictation','recording','file'}. It is a pure membership check at the top of the service, before any audio is written. The message lists the accepted set sorted.

Source

Thrown at backend/services/captures.py:71

        stt_model=row.stt_model,
        llm_model=row.llm_model,
        refinement_flags=flags_model,
        created_at=row.created_at,
    )


async def create_capture(
    *,
    audio_bytes: bytes,
    filename: str,
    source: str,
    language: Optional[str],
    stt_model: Optional[str],
    db: Session,
) -> CaptureResponse:
    """Persist raw audio, run STT, store the row."""
    if source not in VALID_SOURCES:
        raise ValueError(f"Invalid source '{source}'. Must be one of {sorted(VALID_SOURCES)}")

    capture_id = str(uuid.uuid4())
    suffix = Path(filename).suffix.lower() or ".wav"
    if suffix not in (".wav", ".mp3", ".m4a", ".flac", ".ogg", ".webm"):
        suffix = ".wav"

    raw_path = config.get_captures_dir() / f"{capture_id}{suffix}"
    written_files: list[Path] = []

    try:
        raw_path.write_bytes(audio_bytes)
        written_files.append(raw_path)

        # Decode once with librosa — its audioread fallback handles webm/opus
        # via ffmpeg, which miniaudio (used inside mlx-audio's whisper) can't.
        # The decoded array gives us an accurate duration and becomes the
        # canonical WAV we hand to whisper.
        try:

View on GitHub (pinned to 51f49dea19)

Solutions

  1. Send source as exactly one of 'dictation', 'recording', or 'file' (or omit it to default to 'file').
  2. Centralize the allowed source list in a shared constant/enum on the client and sync it with VALID_SOURCES.
  3. Add a client-side dropdown/enum so free-text can't produce an invalid value.

Example fix

// before
const form = new FormData();
form.set('source', 'upload');

// after
const SOURCES = ['dictation', 'recording', 'file'] as const;
form.set('source', SOURCES.includes(userSource) ? userSource : 'file');
Defensive patterns

Strategy: validation

Validate before calling

const CAPTURE_SOURCES = ['dictation', 'recording', 'file'];
function normalizeSource(s) {
  return CAPTURE_SOURCES.includes(s) ? s : 'file';
}
form.set('source', normalizeSource(userSource));

Type guard

function isValidCaptureSource(s) {
  return s === 'dictation' || s === 'recording' || s === 'file';
}

Try / catch

try { await api.createCapture(file, source); }
catch (e) {
  if (e.status === 400 && /Invalid source/.test(e.detail)) {
    source = 'file'; await api.createCapture(file, source); // fall back to default
  } else throw e;
}

Prevention

When it happens

Trigger: POST /captures with a `source` form field value outside the allowed set — e.g. 'upload', 'stream', 'voice', or an empty string. The route defaults source to 'file' when omitted, so this only fires when an explicit bad value is sent.

Common situations: Client sends its own source taxonomy that doesn't match the backend's; a new client build renamed the field values; integration code passing a literal typo.

Related errors


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