perf(startup): lokale Modelle beim Start vorladen (Warm-up)

Lifespan-Hintergrundtask laedt profilabhaengig piper-Stimme + faster-whisper-Modell
vor (nur was die aktive Konfiguration nutzt; Cloud-Profile = No-op). Server ist
sofort verfuegbar; der erste Nutzer zahlt nicht mehr den Kaltstart.

Messung: erster Turn direkt nach Start first_audio 1,62 s (statt ~4,5 s mit Kaltstart).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Dieter Schlüter 2026-06-18 08:27:37 +02:00
commit 111ecd8edf
3 changed files with 84 additions and 1 deletions

45
app/core/warmup.py Normal file
View file

@ -0,0 +1,45 @@
"""Laedt lokale KI-Modelle beim Start vor.
So zahlt nicht der erste Nutzer den Kaltstart (piper ~2 s Modell-Load, faster-whisper
Modell-Load). Es wird nur vorgeladen, was die aktive Konfiguration tatsaechlich nutzt
(Default-Provider) - bei reinen Cloud-Profilen passiert nichts. Best-effort: Fehler
werden geloggt, brechen den Start nie ab.
"""
import io
import logging
import wave
from app.config import settings
logger = logging.getLogger(__name__)
def _silence_wav(seconds: float = 0.1, rate: int = 16000) -> bytes:
buf = io.BytesIO()
with wave.open(buf, "wb") as w:
w.setnchannels(1)
w.setsampwidth(2)
w.setframerate(rate)
w.writeframes(b"\x00\x00" * int(rate * seconds))
return buf.getvalue()
async def warmup_local_models() -> None:
from app.dependencies import get_stt_provider, get_tts_provider
try:
tts = get_tts_provider()
if type(tts).__name__ == "PiperTTSProvider":
await tts.synthesize("Hallo.", audio_format="pcm")
logger.info("warmup: piper-Stimmmodell geladen")
except Exception:
logger.exception("warmup: TTS-Vorladen fehlgeschlagen (ignoriert)")
try:
stt = get_stt_provider()
if type(stt).__name__ == "FasterWhisperProvider":
await stt.transcribe(_silence_wav(), fmt="wav", language=settings.default_language)
logger.info("warmup: faster-whisper-Modell geladen")
except Exception:
logger.exception("warmup: STT-Vorladen fehlgeschlagen (ignoriert)")

View file

@ -1,9 +1,12 @@
import asyncio
import time
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.staticfiles import StaticFiles
from app.core.warmup import warmup_local_models
from app.metrics import metrics
from app.api.health import router as health_router
from app.api.chat import router as chat_router
@ -17,7 +20,17 @@ from app.api.me import router as me_router
from app.api.metrics import router as metrics_router
from app.api.ws import router as ws_router
app = FastAPI(title="Voice Assistant Gateway")
@asynccontextmanager
async def _lifespan(app: FastAPI):
# Lokale Modelle im Hintergrund vorladen -> Server ist sofort verfuegbar,
# der erste Nutzer zahlt nicht den Kaltstart.
task = asyncio.create_task(warmup_local_models())
yield
if not task.done():
task.cancel()
app = FastAPI(title="Voice Assistant Gateway", lifespan=_lifespan)
@app.middleware("http")