perf: pre-warm Whisper + chat model + TTS at bridge startup
The first spoken turn paid a ~10s cold start because Whisper (default "medium") and the Ollama chat model loaded lazily on the first request. Warm them (and ping the TTS worker) in a background thread at startup so the server accepts requests immediately while models load, and the first real utterance is fast. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -525,8 +525,65 @@ def http_converse_stream():
|
||||
return Response(stream_with_context(gen()), mimetype="application/x-ndjson")
|
||||
|
||||
|
||||
def _warm_ollama(base_url: str, model: str) -> None:
|
||||
"""Load ``model`` into Ollama (GPU if available) with a long keep_alive so it
|
||||
is resident before the first real turn. Best-effort."""
|
||||
if not base_url or not model:
|
||||
return
|
||||
import urllib.request
|
||||
|
||||
try:
|
||||
req = urllib.request.Request(
|
||||
f"{base_url.rstrip('/')}/api/generate",
|
||||
data=json.dumps(
|
||||
{"model": model, "prompt": "", "stream": False,
|
||||
"keep_alive": "30m", "options": {"num_predict": 1}}
|
||||
).encode("utf-8"),
|
||||
headers={"Content-Type": "application/json"},
|
||||
)
|
||||
with urllib.request.urlopen(req, timeout=120) as resp:
|
||||
ok = resp.status == 200
|
||||
print(f"[bridge] {'✅' if ok else '⚠️'} ollama warm (model={model})", flush=True)
|
||||
except Exception as e: # pragma: no cover - depends on local ollama
|
||||
print(f"[bridge] ollama warmup skipped (model={model}): {e}", flush=True)
|
||||
|
||||
|
||||
def _warmup() -> None:
|
||||
"""Pre-load Whisper + the chat model + TTS so the FIRST real utterance does
|
||||
not pay the cold-start cost (observed ~10s on the first STT). Best-effort and
|
||||
runs in a background thread so the HTTP server (and /health) is up
|
||||
immediately."""
|
||||
try:
|
||||
_ensure_brain()
|
||||
# JIT the Whisper transcribe path on a short silent buffer. We call the
|
||||
# model directly (not transcribe()) because the VAD gate short-circuits
|
||||
# silence before Whisper would run, leaving the model un-warmed.
|
||||
if _whisper is not None:
|
||||
try:
|
||||
import numpy as np
|
||||
|
||||
dummy = np.zeros(8000, dtype=np.float32) # 0.5s @ 16kHz
|
||||
segs, _info = _whisper.transcribe(dummy, beam_size=1, language=STT_LANGUAGE)
|
||||
for _ in segs:
|
||||
pass
|
||||
print("[bridge] ✅ whisper warm", flush=True)
|
||||
except Exception as e: # pragma: no cover
|
||||
print(f"[bridge] whisper warmup skipped: {e}", flush=True)
|
||||
if _cfg is not None:
|
||||
_warm_ollama(getattr(_cfg, "ollama_base_url", ""), getattr(_cfg, "ollama_chat_model", ""))
|
||||
# Nudge the TTS worker to warm (MeloTTS loads its model before binding
|
||||
# its port, so a ready ping confirms it; Piper loads on first synth).
|
||||
if _tts_ready():
|
||||
print("[bridge] ✅ tts warm", flush=True)
|
||||
except Exception as e: # pragma: no cover
|
||||
print(f"[bridge] warmup error: {e}", flush=True)
|
||||
|
||||
|
||||
def main():
|
||||
print(f"[bridge] listening on http://{BRIDGE_HOST}:{BRIDGE_PORT}", flush=True)
|
||||
# Warm the models in the background so the first spoken turn is fast while
|
||||
# the server is already accepting requests.
|
||||
threading.Thread(target=_warmup, name="bridge-warmup", daemon=True).start()
|
||||
# threaded=True so STT (slow) on one request doesn't block /health, etc.
|
||||
app.run(host=BRIDGE_HOST, port=BRIDGE_PORT, threaded=True)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user