{"record":{"id":"112deebca6842651","repo":"odysseus-dev/odysseus","slug":"transcription-failed-112dee","errorCode":null,"errorMessage":"Transcription failed","messagePattern":"Transcription failed","errorType":"http","errorClass":"HTTPException","httpStatus":500,"severity":"error","filePath":"routes/stt_routes.py","lineNumber":41,"sourceCode":"            raise HTTPException(status_code=500, detail=str(e))\n\n    @router.post(\"/transcribe\")\n    async def transcribe_audio(file: UploadFile = File(...)):\n        \"\"\"Transcribe uploaded audio file to text\"\"\"\n        try:\n            if not stt_service.available:\n                raise HTTPException(\n                    status_code=503,\n                    detail={\"message\": \"STT service not available or set to browser mode\"}\n                )\n\n            audio_bytes = await read_upload_limited(file, STT_MAX_AUDIO_BYTES, \"Audio file\")\n            if not audio_bytes:\n                raise HTTPException(status_code=400, detail={\"message\": \"Empty audio file\"})\n\n            text = stt_service.transcribe(audio_bytes)\n            if text is None:\n                raise HTTPException(\n                    status_code=500,\n                    detail={\"message\": \"Transcription failed\"}\n                )\n\n            return {\"text\": text}\n\n        except HTTPException:\n            raise\n        except Exception as e:\n            logger.error(f\"Transcription error: {e}\", exc_info=True)\n            raise HTTPException(\n                status_code=500,\n                detail={\"message\": f\"Transcription failed: {str(e)}\"}\n            )\n\n    return router\n","sourceCodeStart":23,"sourceCodeEnd":58,"githubUrl":"https://github.com/odysseus-dev/odysseus/blob/f9235ebbf13f693a6fd29ce70b097f6ec83705bf/routes/stt_routes.py#L23-L58","documentation":"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\"}.","triggerScenarios":"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.","commonSituations":"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).","solutions":["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.","Verify the audio format is supported: convert the clip to WAV/MP3 (16 kHz mono WAV is safest) and retry.","Confirm the STT model artifacts exist and load: check the stt_service configuration (model path/name) and that the container/host has ffmpeg installed.","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."],"exampleFix":"// before: caller cannot tell why it failed\nconst r = await fetch('/api/stt/transcribe', {method:'POST', body: fd});\nif (!r.ok) throw new Error('failed');\n\n// after: surface the structured detail\nconst r = await fetch('/api/stt/transcribe', {method:'POST', body: fd});\nif (r.status === 500) {\n  const {detail} = await r.json();\n  throw new Error(detail?.message ?? 'Transcription failed');\n}","handlingStrategy":"fallback","validationCode":"const MAX = 25 * 1024 * 1024; // match STT_MAX_AUDIO_BYTES\nasync function preflight(file) {\n  if (!file || file.size === 0) throw new Error('Empty audio file');\n  if (file.size > MAX) throw new Error('Audio too large');\n  if (!/^(audio|video)\\//.test(file.type) && !/\\.(wav|mp3|m4a|ogg|webm)$/i.test(file.name))\n    throw new Error('Unsupported audio format');\n  const health = await fetch('/api/stt/health'); // if exposed\n  return health.ok;\n}","typeGuard":"function isTranscribeOk(body: unknown): body is { text: string } {\n  return typeof body === 'object' && body !== null && typeof (body as any).text === 'string' && (body as any).text.length >= 0;\n}","tryCatchPattern":"try {\n  const res = await fetch('/api/stt/transcribe', {method: 'POST', body: fd});\n  if (res.status === 503) return useBrowserStt();       // service off\n  if (res.status === 400) return alertUser('Bad audio');\n  if (res.status === 500) return useBrowserStt();       // transcribe() returned None\n  return (await res.json()).text;\n} catch (e) {\n  return useBrowserStt(); // network-level fallback\n}","preventionTips":["Convert uploads to 16 kHz mono WAV client-side (e.g. WebAudio downmix) before POSTing.","Guard the endpoint in UI behind the STT availability flag so users never reach a 500.","Keep a browser-based SpeechRecognition fallback for when server transcription fails.","Alert on 500-rate of this route: transcribe() returning None usually means model/env drift."],"tags":["stt","audio","fastapi","http-500","ml-inference"],"backgroundTag":null,"analyzedSha":"f9235ebbf13f693a6fd29ce70b097f6ec83705bf","analyzedAt":"2026-08-14T21:47:48.359Z","schemaVersion":2},"datasetVersion":"2026-08-15T22:17:37.221Z"}