Fix STT health check incorrectly marking Open WebUI as whisper.cpp

Port 8080 is Open WebUI — it passes /health + /v1/models checks but
returns 405 on POST /v1/audio/transcriptions. Updated probe logic to:
- treat 405 as 'endpoint missing, try next path'
- treat non-JSON 500 as broken, JSON-500 with detail as 'audio too short' (ok)
- use 500ms silence WAV instead of 1-frame (too tiny for alignment models)

Changed whisper.cpp default from :8080 to :8085 to avoid clash with
Open WebUI. Updated s-llms.html placeholder and code snippet accordingly.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
mARTin-B78 2026-05-28 09:03:21 +02:00
parent 8f9060d025
commit d16be50cc9
2 changed files with 31 additions and 14 deletions

View File

@ -48,7 +48,7 @@ _NVIDIA_CLONE_DEFAULT = os.environ.get("NVIDIA_TTS_CLONE_URL", "http://host.dock
_NVIDIA_ZEROSHOT_DEFAULT = os.environ.get("NVIDIA_ZEROSHOT_TTS_URL", _NVIDIA_CLONE_DEFAULT) _NVIDIA_ZEROSHOT_DEFAULT = os.environ.get("NVIDIA_ZEROSHOT_TTS_URL", _NVIDIA_CLONE_DEFAULT)
_NVIDIA_FLOW_DEFAULT = os.environ.get("NVIDIA_FLOW_TTS_URL", "http://host.docker.internal:8094") _NVIDIA_FLOW_DEFAULT = os.environ.get("NVIDIA_FLOW_TTS_URL", "http://host.docker.internal:8094")
_FASTER_WHISPER_DEFAULT = os.environ.get("FASTER_WHISPER_URL", "http://host.docker.internal:8000") _FASTER_WHISPER_DEFAULT = os.environ.get("FASTER_WHISPER_URL", "http://host.docker.internal:8000")
_WHISPER_CPP_DEFAULT = os.environ.get("WHISPER_CPP_URL", "http://host.docker.internal:8080") _WHISPER_CPP_DEFAULT = os.environ.get("WHISPER_CPP_URL", "http://host.docker.internal:8085")
_GROQ_STT_ENDPOINT = "https://api.groq.com/openai/v1" _GROQ_STT_ENDPOINT = "https://api.groq.com/openai/v1"
_KOKORO_DEFAULT = os.environ.get("KOKORO_URL", "http://host.docker.internal:8880/v1") _KOKORO_DEFAULT = os.environ.get("KOKORO_URL", "http://host.docker.internal:8880/v1")
_VIBEVOICE_DEFAULT = os.environ.get("VIBEVOICE_URL", "http://192.168.178.8:8027") _VIBEVOICE_DEFAULT = os.environ.get("VIBEVOICE_URL", "http://192.168.178.8:8027")
@ -1903,15 +1903,15 @@ 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: def _make_minimal_wav(duration_ms: int = 500) -> bytes:
"""44-byte WAV header with 1 frame of silence — smallest valid WAV.""" """Minimal WAV: mono 16-bit 16kHz silence of given duration."""
import struct sample_rate = 16000
num_frames = 1 num_frames = sample_rate * duration_ms // 1000
data = b"\x00\x00" data = b"\x00\x00" * num_frames
header = struct.pack( header = struct.pack(
"<4sI4s4sIHHIIHH4sI", "<4sI4s4sIHHIIHH4sI",
b"RIFF", 36 + len(data), b"WAVE", b"RIFF", 36 + len(data), b"WAVE",
b"fmt ", 16, 1, 1, 16000, 32000, 2, 16, b"fmt ", 16, 1, 1, sample_rate, sample_rate * 2, 2, 16,
b"data", len(data), b"data", len(data),
) )
return header + data return header + data
@ -1942,9 +1942,11 @@ def _stt_backend_health(url: str) -> tuple[bool, list[str]]:
if not ok: if not ok:
return False, models return False, models
# Verify the transcription endpoint actually works (catches broken builds like # Verify the transcription endpoint actually works (catches broken builds like
# CTranslate2 containers compiled without CUDA that pass /health but fail on load) # CTranslate2 containers compiled without CUDA that pass /health but fail on load,
# and LLM servers that expose /v1/models but no transcription endpoint)
try: try:
wav = _make_minimal_wav() wav = _make_minimal_wav()
endpoint_found = False
for path in ("/v1/audio/transcriptions", "/transcribe"): for path in ("/v1/audio/transcriptions", "/transcribe"):
try: try:
r = requests.post( r = requests.post(
@ -1953,14 +1955,29 @@ def _stt_backend_health(url: str) -> tuple[bool, list[str]]:
data={"model": "whisper-1", "response_format": "text"}, data={"model": "whisper-1", "response_format": "text"},
timeout=8, timeout=8,
) )
if r.status_code in {404, 405}:
continue # try next path — endpoint missing or wrong method
if r.status_code == 500: if r.status_code == 500:
ok = False # backend is broken (model load failed etc.) # Distinguish backend crash from "couldn't process empty audio":
break # 200, 400, 404, 422 all mean the endpoint exists and model loaded # a JSON detail that isn't the generic FastAPI text means the
# model loaded and responded — treat as ok (audio was just bad)
try:
detail = r.json().get("detail", "")
ok = bool(detail) and detail.lower() != "internal server error"
except Exception:
ok = False # non-JSON 500 → backend likely broken
else:
ok = True # 200, 400, 415, 422 → endpoint works
endpoint_found = True
break
except requests.exceptions.ConnectionError: except requests.exceptions.ConnectionError:
ok = False ok = False
endpoint_found = True
break break
except Exception: except Exception:
pass # timeout or other — don't mark as broken, just skip pass # timeout — don't penalise slow model loads
if not endpoint_found:
ok = False # all paths returned 404/405 — not an STT backend
except Exception: except Exception:
pass pass
return ok, models return ok, models

View File

@ -350,7 +350,7 @@ Check "Enable CORS" for browser access</pre>
<p class="llm-local-desc">Minimal C++ Whisper with a built-in HTTP server. CPU or Metal/CUDA. Low memory, fast on consumer hardware.</p> <p class="llm-local-desc">Minimal C++ Whisper with a built-in HTTP server. CPU or Metal/CUDA. Low memory, fast on consumer hardware.</p>
<div class="llm-local-url"> <div class="llm-local-url">
<span class="llm-local-url-label">URL</span> <span class="llm-local-url-label">URL</span>
<input class="llm-local-url-inp" type="text" placeholder="http://localhost:8080" data-llm-local-key="whisper-cpp" data-llm-local-default="http://localhost:8080" spellcheck="false"> <input class="llm-local-url-inp" type="text" placeholder="http://localhost:8085" data-llm-local-key="whisper-cpp" data-llm-local-default="http://localhost:8085" spellcheck="false">
<button class="llm-local-ping" data-ping-key="whisper-cpp" title="Test connection">Connect</button> <button class="llm-local-ping" data-ping-key="whisper-cpp" title="Test connection">Connect</button>
</div> </div>
<div class="llm-local-actions"> <div class="llm-local-actions">
@ -359,12 +359,12 @@ Check "Enable CORS" for browser access</pre>
<div class="llm-local-snippet"> <div class="llm-local-snippet">
<div class="llm-snippet-bar"> <div class="llm-snippet-bar">
<span>Build &amp; run</span> <span>Build &amp; run</span>
<button class="llm-copy-btn" data-copy="git clone https://github.com/ggml-org/whisper.cpp&#10;cd whisper.cpp && cmake -B build && cmake --build build -j&#10;./build/bin/whisper-server -m models/ggml-large-v3.bin --port 8080">Copy</button> <button class="llm-copy-btn" data-copy="git clone https://github.com/ggml-org/whisper.cpp&#10;cd whisper.cpp && cmake -B build && cmake --build build -j&#10;./build/bin/whisper-server -m models/ggml-large-v3.bin --port 8085">Copy</button>
</div> </div>
<pre>git clone https://github.com/ggml-org/whisper.cpp <pre>git clone https://github.com/ggml-org/whisper.cpp
cd whisper.cpp && cmake -B build && cmake --build build -j cd whisper.cpp && cmake -B build && cmake --build build -j
./build/bin/whisper-server \ ./build/bin/whisper-server \
-m models/ggml-large-v3.bin --port 8080</pre> -m models/ggml-large-v3.bin --port 8085</pre>
</div> </div>
<a class="llm-local-link" href="https://github.com/ggml-org/whisper.cpp" target="_blank" rel="noopener">github.com/ggml-org/whisper.cpp <span class="mdi mdi-open-in-new link-icon"></span></a> <a class="llm-local-link" href="https://github.com/ggml-org/whisper.cpp" target="_blank" rel="noopener">github.com/ggml-org/whisper.cpp <span class="mdi mdi-open-in-new link-icon"></span></a>
</div> </div>