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:
parent
28361f2db6
commit
8f9060d025
42
server.py
42
server.py
@ -1903,6 +1903,20 @@ def _stt_backend_api_key(settings: dict, backend: str) -> str:
|
|||||||
return settings.get("whisper_api_key", "").strip()
|
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]]:
|
def _stt_backend_health(url: str) -> tuple[bool, list[str]]:
|
||||||
base = _validate_http_url(url, allow_private=True).rstrip("/")
|
base = _validate_http_url(url, allow_private=True).rstrip("/")
|
||||||
models: list[str] = []
|
models: list[str] = []
|
||||||
@ -1925,6 +1939,30 @@ def _stt_backend_health(url: str) -> tuple[bool, list[str]]:
|
|||||||
models.append(item)
|
models.append(item)
|
||||||
except Exception:
|
except Exception:
|
||||||
pass
|
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
|
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)
|
detail = body.get("detail") or body.get("error") or body.get("message") or str(body)
|
||||||
except Exception:
|
except Exception:
|
||||||
detail = resp.text[:300].strip()
|
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
|
return _transcription_text_from_response(resp), backend
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user