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:
javis-bot
2026-06-14 00:04:03 +09:00
parent de5384d166
commit d4e5e7f3f7

View File

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