odysseus-dev/odysseus · error · HTTPException

Transcription failed

Error message

Transcription failed

What it means

Raised by the STT (speech-to-text) upload endpoint when stt_service.transcribe(audio_bytes) returns None instead of a transcription string. The service object reported itself as available, but the transcription step itself failed internally (model load failure, unsupported/corrupt audio, decode error) and signals failure by returning None. The API maps that sentinel to HTTP 500 with detail {"message": "Transcription failed"}.

Source

Thrown at routes/stt_routes.py:41

            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)}"}
            )

    return router

View on GitHub (pinned to f9235ebbf1)

Solutions

  1. Check server logs: the companion handler at stt_routes.py logs 'Transcription error: ...' with exc_info for unexpected exceptions — read the traceback to find the real cause.
  2. Verify the audio format is supported: convert the clip to WAV/MP3 (16 kHz mono WAV is safest) and retry.
  3. Confirm the STT model artifacts exist and load: check the stt_service configuration (model path/name) and that the container/host has ffmpeg installed.
  4. If transcription keeps returning None on valid input, inspect stt_service.transcribe for swallowed exceptions and make it raise or log the underlying error instead of returning None.

Example fix

// before: caller cannot tell why it failed
const r = await fetch('/api/stt/transcribe', {method:'POST', body: fd});
if (!r.ok) throw new Error('failed');

// after: surface the structured detail
const r = await fetch('/api/stt/transcribe', {method:'POST', body: fd});
if (r.status === 500) {
  const {detail} = await r.json();
  throw new Error(detail?.message ?? 'Transcription failed');
}
Defensive patterns

Strategy: fallback

Validate before calling

const MAX = 25 * 1024 * 1024; // match STT_MAX_AUDIO_BYTES
async function preflight(file) {
  if (!file || file.size === 0) throw new Error('Empty audio file');
  if (file.size > MAX) throw new Error('Audio too large');
  if (!/^(audio|video)\//.test(file.type) && !/\.(wav|mp3|m4a|ogg|webm)$/i.test(file.name))
    throw new Error('Unsupported audio format');
  const health = await fetch('/api/stt/health'); // if exposed
  return health.ok;
}

Type guard

function isTranscribeOk(body: unknown): body is { text: string } {
  return typeof body === 'object' && body !== null && typeof (body as any).text === 'string' && (body as any).text.length >= 0;
}

Try / catch

try {
  const res = await fetch('/api/stt/transcribe', {method: 'POST', body: fd});
  if (res.status === 503) return useBrowserStt();       // service off
  if (res.status === 400) return alertUser('Bad audio');
  if (res.status === 500) return useBrowserStt();       // transcribe() returned None
  return (await res.json()).text;
} catch (e) {
  return useBrowserStt(); // network-level fallback
}

Prevention

When it happens

Trigger: POST to the /api/stt transcribe route with a multipart audio file while stt_service.available is truthy, but transcribe() returns None — e.g. whisper model missing/corrupt on disk, audio container/codec the backend cannot decode, empty audio stream after a non-zero-length upload, or an OOM/exception swallowed inside the service that returns None.

Common situations: STT backend set to a local model path that does not exist; whisper weights downloaded for a different architecture; uploading webm/ogg clips when only wav is supported; GPU out of memory during inference; docker image built without the audio decode system libs (ffmpeg).

Related errors


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