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 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-05-28 08:31:28 +02:00
parent 28361f2db6
commit 8f9060d025

View File

@ -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