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
- Client-side, refuse to upload when the blob size is 0 — check file.size before POSTing.
- For recordings, enforce a minimum capture duration before allowing stop/upload.
- 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
- Check blob/file size client-side before POSTing; skip empty recordings.
- Enforce a minimum recording duration on push-to-talk UIs.
- Reset file read pointers before constructing multipart bodies.
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.