From d4e5e7f3f738b0ba3941970ac6b8d39e2297077d Mon Sep 17 00:00:00 2001 From: javis-bot Date: Sun, 14 Jun 2026 00:04:03 +0900 Subject: [PATCH] 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 --- bridge/server.py | 57 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/bridge/server.py b/bridge/server.py index e33199a..e05095d 100644 --- a/bridge/server.py +++ b/bridge/server.py @@ -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)