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
- Send source as exactly one of 'dictation', 'recording', or 'file' (or omit it to default to 'file').
- Centralize the allowed source list in a shared constant/enum on the client and sync it with VALID_SOURCES.
- 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
- Send source from a fixed enum shared with the backend's VALID_SOURCES.
- Omit the field to accept the 'file' default rather than guessing.
- Keep the client's source taxonomy in sync after backend changes.
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
- Story item not found or invalid split point
- Story item or version not found
- Story has no audio items
- Could not decode {suffix} audio — the recording may be empty
- Channel with name '{data.name}' already exists
AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12).
Data as JSON: /api/errors/cb8080cb3a01d925.
Report an issue: GitHub.