odysseus-dev/odysseus · error · HTTPException
Transcription failed: {str(e)}
Error message
Transcription failed: {str(e)} What it means
Raised by the same STT endpoint when an unexpected (non-HTTPException) exception escapes the request body — the generic except Exception handler at stt_routes.py wraps it into HTTP 500 with detail "Transcription failed: {str(e)}". The embedded str(e) is the original exception message, so the body carries the root cause while the server log carries the full traceback via logger.error(..., exc_info=True).
Source
Thrown at routes/stt_routes.py:52
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)}"}
)
return router
View on GitHub (pinned to f9235ebbf1)
Solutions
- Read the message text after 'Transcription failed:' — it names the underlying exception (e.g. 'ffmpeg returned non-zero exit status', 'CUDA out of memory').
- Match that message to the fix: install ffmpeg / free GPU memory / fix temp-dir permissions.
- Correlate with the server log entry 'Transcription error: {e}' (full traceback) for the stack trace.
- If the exception text leaks sensitive paths, wrap detail as a generic message server-side and keep specifics in logs only.
Example fix
# before
except Exception as e:
raise HTTPException(500, f"Transcription failed: {str(e)}")
# after: keep detail in logs, return stable message
except Exception as e:
logger.error(f"Transcription error: {e}", exc_info=True)
raise HTTPException(500, {"message": "Transcription failed", "error_id": log_context_id}) Defensive patterns
Strategy: try-catch
Type guard
function isSttErrorDetail(v: unknown): v is { message: string } {
return typeof v === 'object' && v !== null && typeof (v as any).message === 'string';
} Try / catch
try {
const res = await fetch('/api/stt/transcribe', {method:'POST', body: fd});
if (!res.ok) {
const d = await res.json().catch(() => null);
const msg = d?.detail?.message ?? `HTTP ${res.status}`;
logToTelemetry('stt_failed', {msg});
throw new TranscriptionError(msg);
}
} catch (e) { /* single place: retry once, then fall back to browser STT */ } Prevention
- Wrap the upload in a timeout so truncated multipart bodies never reach the handler.
- Smoke-test STT on deploy: send a 1-second silent WAV and assert 200.
- Do not surface raw str(e) to end users; log it server-side with exc_info (already done) and show a generic message client-side.
- Pin ffmpeg/av library versions in the deployment image.
When it happens
Trigger: Any unhandled exception inside the route after the HTTPException guards: read_upload_limited raising a non-HTTP error (I/O hiccup, malformed multipart), stt_service.transcribe raising (model crash, CUDA error), or a bug in response serialization. The client sees 500 with the exception text appended.
Common situations: Multipart body truncated mid-upload; ffmpeg/av library missing so decode raises instead of returning None; CUDA device assert; temporary file permission errors in the upload spool directory; version mismatch between the audio decode lib and the model wrapper.
Related errors
- Transcription failed
- Failed to delete calendar
- Failed to list calendars
- Failed to list events
- Failed to create event
AI-assisted analysis of odysseus-dev/odysseus@f9235ebbf1 (2026-08-14).
Data as JSON: /api/errors/61c4b453e0594880.
Report an issue: GitHub.