jamiepine/voicebox · warning · ValueError
Could not decode {suffix} audio — the recording may be empty
Error message
Could not decode {suffix} audio — the recording may be empty or corrupt What it means
Raised as ValueError (HTTP 400) inside create_capture when librosa-based load_audio fails to decode the uploaded audio AND the file's suffix is not in WHISPER_NATIVE_FORMATS = ('.wav','.mp3','.flac','.ogg'). The service first tries to decode with load_audio; on failure it only passes the raw file through to Whisper if Whisper's miniaudio loader can read that format natively. For webm/m4a/etc. it surfaces this clean error instead of letting Whisper 500 later. Root cause is empty, truncated, or genuinely corrupt audio in a non-native container.
Source
Thrown at backend/services/captures.py:104
# 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:
audio, sr = load_audio(str(raw_path))
duration_ms = int((len(audio) / sr) * 1000) if sr else None
except Exception as decode_err:
logger.warning(
"Could not decode capture %s (%s): %r", capture_id, suffix, decode_err
)
audio, sr = None, None
duration_ms = None
if audio is None or sr is None:
# Decode failed. Only pass the file straight to whisper if the
# source is a format its miniaudio loader can still read — webm,
# m4a, etc. would just 500 later. Surface a clean error instead.
if suffix not in WHISPER_NATIVE_FORMATS:
raise ValueError(
f"Could not decode {suffix} audio — the recording may be empty or corrupt"
)
audio_path = raw_path
elif suffix == ".wav":
audio_path = raw_path
else:
# Transcode to WAV so downstream loaders (miniaudio, soundfile) work
# regardless of what format the client shipped.
audio_path = config.get_captures_dir() / f"{capture_id}.wav"
sf.write(str(audio_path), audio, sr, format="WAV")
written_files.append(audio_path)
with contextlib.suppress(OSError):
raw_path.unlink()
written_files.remove(raw_path)
whisper = get_whisper_model()
resolved_stt = stt_model or whisper.model_size
transcript = await whisper.transcribe(str(audio_path), language, resolved_stt)View on GitHub (pinned to 51f49dea19)
Solutions
- On the client, reject empty/very-small recordings before uploading.
- Ensure ffmpeg is installed on the server so librosa can decode webm/opus/m4a via audioread.
- If the source format is controllable, upload .wav/.mp3/.flac/.ogg which can fall through to Whisper directly.
- Re-record; if persistently failing for one file, treat it as corrupt.
Example fix
// before
const blob = recorder.getBlob(); // possibly empty
upload(blob);
// after
const blob = recorder.getBlob();
if (!blob || blob.size < 1024) {
toast('Recording is empty; please try again');
return;
}
upload(blob); Defensive patterns
Strategy: validation
Validate before calling
function isLikelyValidRecording(blob) {
if (!blob || blob.size < 1024) return false; // empty/truncated
if (!/\.(wav|mp3|m4a|ogg|flac|aac|webm|opus)$/i.test(blob.name || '')) return false;
return true;
}
if (!isLikelyValidRecording(blob)) { notify('Recording is empty or unsupported'); return; } Type guard
function isDecodableBlob(blob, hasFfmpeg) {
if (blob.size < 1024) return false;
const native = /\.(wav|mp3|flac|ogg)$/i;
return native.test(blob.name || '') || hasFfmpeg;
} Try / catch
try { await api.createCapture(blob, 'recording'); }
catch (e) {
if (e.status === 400 && /decode/i.test(e.detail)) {
notify('Recording was empty or corrupt; please re-record');
} else throw e;
} Prevention
- Discard empty/very short recordings client-side before upload.
- Ensure ffmpeg is installed on the server for webm/opus/m4a decode via librosa/audioread.
- Prefer uploading wav/mp3/flac/ogg when the host can't guarantee ffmpeg.
When it happens
Trigger: Uploading a .webm/.m4a/.opus recording that is empty (0 bytes of audio), truncated (recording stopped mid-write), or encoded with a codec the host's ffmpeg/librosa can't decode. The decoded audio array comes back None and the suffix isn't a Whisper-native format, so the guard fires.
Common situations: Browser MediaRecorder produced an empty blob (user never spoke / permission glitch), upload was cut off, the host lacks ffmpeg so librosa's audioread fallback can't decode webm/opus, or a transcode step upstream produced a malformed file.
Related errors
- Invalid source '{source}'. Must be one of {sorted(VALID_SOUR
- captures.noTranscriptError
- `limit` must be between 1 and 200.
- `offset` must be >= 0.
- Unsupported audio format '{suffix}'. Allowed: {sorted(IMPORT
AI-assisted analysis of jamiepine/voicebox@51f49dea19 (2026-08-12).
Data as JSON: /api/errors/a51cbb13e26efbf6.
Report an issue: GitHub.