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

  1. Read the message text after 'Transcription failed:' — it names the underlying exception (e.g. 'ffmpeg returned non-zero exit status', 'CUDA out of memory').
  2. Match that message to the fix: install ffmpeg / free GPU memory / fix temp-dir permissions.
  3. Correlate with the server log entry 'Transcription error: {e}' (full traceback) for the stack trace.
  4. 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

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


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