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

  1. On the client, reject empty/very-small recordings before uploading.
  2. Ensure ffmpeg is installed on the server so librosa can decode webm/opus/m4a via audioread.
  3. If the source format is controllable, upload .wav/.mp3/.flac/.ogg which can fall through to Whisper directly.
  4. 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

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


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