From 8f9060d0252ff2cf742490452d0241548933343e Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Thu, 28 May 2026 08:31:28 +0200 Subject: [PATCH] Detect broken STT backends via real transcription probe in health check _stt_backend_health now sends a minimal WAV to the transcription endpoint after passing /health. A 500 response marks the backend unavailable, catching containers that pass health checks but crash on model load (e.g. CTranslate2 built without CUDA support). Error messages from _transcribe_audio now include the backend URL and replace generic 'Internal Server Error' with an actionable explanation. Co-Authored-By: Claude Sonnet 4.6 --- server.py | 42 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 41 insertions(+), 1 deletion(-) diff --git a/server.py b/server.py index bd35eb8..6674a27 100644 --- a/server.py +++ b/server.py @@ -1903,6 +1903,20 @@ def _stt_backend_api_key(settings: dict, backend: str) -> str: return settings.get("whisper_api_key", "").strip() +def _make_minimal_wav() -> bytes: + """44-byte WAV header with 1 frame of silence — smallest valid WAV.""" + import struct + num_frames = 1 + data = b"\x00\x00" + header = struct.pack( + "<4sI4s4sIHHIIHH4sI", + b"RIFF", 36 + len(data), b"WAVE", + b"fmt ", 16, 1, 1, 16000, 32000, 2, 16, + b"data", len(data), + ) + return header + data + + def _stt_backend_health(url: str) -> tuple[bool, list[str]]: base = _validate_http_url(url, allow_private=True).rstrip("/") models: list[str] = [] @@ -1925,6 +1939,30 @@ def _stt_backend_health(url: str) -> tuple[bool, list[str]]: models.append(item) except Exception: pass + if not ok: + return False, models + # Verify the transcription endpoint actually works (catches broken builds like + # CTranslate2 containers compiled without CUDA that pass /health but fail on load) + try: + wav = _make_minimal_wav() + for path in ("/v1/audio/transcriptions", "/transcribe"): + try: + r = requests.post( + f"{base}{path}", + files={"file": ("probe.wav", wav, "audio/wav")}, + data={"model": "whisper-1", "response_format": "text"}, + timeout=8, + ) + if r.status_code == 500: + ok = False # backend is broken (model load failed etc.) + break # 200, 400, 404, 422 all mean the endpoint exists and model loaded + except requests.exceptions.ConnectionError: + ok = False + break + except Exception: + pass # timeout or other — don't mark as broken, just skip + except Exception: + pass return ok, models @@ -2039,7 +2077,9 @@ def _transcribe_audio(src: Path, settings: dict, backend: str = "configured") -> detail = body.get("detail") or body.get("error") or body.get("message") or str(body) except Exception: detail = resp.text[:300].strip() - raise RuntimeError(f"STT backend returned HTTP {resp.status_code}: {detail}") + if not detail or detail.lower() in {"internal server error", "unknown error"}: + detail = f"HTTP {resp.status_code} — backend may be misconfigured or missing CUDA support" + raise RuntimeError(f"STT ({stt_url}): {detail}") return _transcription_text_from_response(resp), backend