odysseus-dev/odysseus · warning · HTTPException

Empty audio file

Error message

Empty audio file

What it means

400 from POST /api/stt/transcribe: read_upload_limited returned no bytes — the uploaded file part is present but its content is zero-length (or drained to zero) after reading within STT_MAX_AUDIO_BYTES. This is an input-shape error raised before the audio ever reaches the STT engine.

Source

Thrown at routes/stt_routes.py:37

        try:
            return stt_service.get_stats()
        except Exception as e:
            logger.error(f"Failed to get STT stats: {e}")
            raise HTTPException(status_code=500, detail=str(e))

    @router.post("/transcribe")
    async def transcribe_audio(file: UploadFile = File(...)):
        """Transcribe uploaded audio file to text"""
        try:
            if not stt_service.available:
                raise HTTPException(
                    status_code=503,
                    detail={"message": "STT service not available or set to browser mode"}
                )

            audio_bytes = await read_upload_limited(file, STT_MAX_AUDIO_BYTES, "Audio file")
            if not audio_bytes:
                raise HTTPException(status_code=400, detail={"message": "Empty audio file"})

            text = stt_service.transcribe(audio_bytes)
            if text is None:
                raise HTTPException(
                    status_code=500,
                    detail={"message": "Transcription failed"}
                )

            return {"text": text}

        except HTTPException:
            raise
        except Exception as e:
            logger.error(f"Transcription error: {e}", exc_info=True)
            raise HTTPException(
                status_code=500,
                detail={"message": f"Transcription failed: {str(e)}"}
            )

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Client-side, refuse to upload when the blob size is 0 — check file.size before POSTing.
  2. For recordings, enforce a minimum capture duration before allowing stop/upload.
  3. If a real file uploads as empty, inspect the multipart construction (filename set, read pointer reset, stream not already consumed).

Example fix

# before
client.post("/api/stt/transcribe", files={"file": ("a.wav", blob, "audio/wav")})  # 400

# after
if blob.size == 0:
    raise ValueError("no audio captured — hold the button longer")
client.post("/api/stt/transcribe", files={"file": ("a.wav", blob, "audio/wav")})
Defensive patterns

Strategy: validation

Validate before calling

data = audio_blob.getvalue() if hasattr(audio_blob, "getvalue") else audio_blob
assert len(data) > 0, "refusing to upload zero-length audio"

Type guard

def has_audio_payload(file_obj) -> bool:
    """True when the upload part carries at least one byte."""
    try:
        pos = file_obj.tell()
        size = file_obj.seek(0, 2) - pos if pos is not None else file_obj.seek(0, 2)
        file_obj.seek(0)
        return size > 0
    except (OSError, AttributeError):
        return False

Prevention

When it happens

Trigger: Uploading a 0-byte file (touch empty.wav); a client bug creating a Blob/File with no data; a MediaRecorder stream uploaded before any audio was captured (instant stop); a multipart part with an empty body.

Common situations: Push-to-talk released instantly producing an empty recording; file picker on a placeholder/empty temp file; upstream proxy stripping the body.

Related errors


AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14). Data as JSON: /api/errors/67625987097de7a0. Report an issue: GitHub.