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:
parent
7ca69e1049
commit
111ecd8edf
3 changed files with 84 additions and 1 deletions
45
app/core/warmup.py
Normal file
45
app/core/warmup.py
Normal 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)")
|
||||||
15
app/main.py
15
app/main.py
|
|
@ -1,9 +1,12 @@
|
||||||
|
import asyncio
|
||||||
import time
|
import time
|
||||||
|
from contextlib import asynccontextmanager
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from fastapi import FastAPI, Request
|
from fastapi import FastAPI, Request
|
||||||
from fastapi.staticfiles import StaticFiles
|
from fastapi.staticfiles import StaticFiles
|
||||||
|
|
||||||
|
from app.core.warmup import warmup_local_models
|
||||||
from app.metrics import metrics
|
from app.metrics import metrics
|
||||||
from app.api.health import router as health_router
|
from app.api.health import router as health_router
|
||||||
from app.api.chat import router as chat_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.metrics import router as metrics_router
|
||||||
from app.api.ws import router as ws_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")
|
@app.middleware("http")
|
||||||
|
|
|
||||||
25
tests/test_warmup.py
Normal file
25
tests/test_warmup.py
Normal file
|
|
@ -0,0 +1,25 @@
|
||||||
|
import asyncio
|
||||||
|
|
||||||
|
from app.core.warmup import warmup_local_models
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_is_noop_for_cloud_defaults():
|
||||||
|
# Mit den Test-Defaults (openrouter-Provider) lädt warmup nichts und wirft nicht.
|
||||||
|
asyncio.run(warmup_local_models())
|
||||||
|
|
||||||
|
|
||||||
|
def test_warmup_loads_piper(monkeypatch):
|
||||||
|
calls = {}
|
||||||
|
|
||||||
|
class FakePiperTTSProvider: # Name muss zur Typpruefung im warmup passen
|
||||||
|
async def synthesize(self, text, voice=None, audio_format="pcm"):
|
||||||
|
calls["tts"] = text
|
||||||
|
return b"AUDIO"
|
||||||
|
|
||||||
|
import app.dependencies as deps
|
||||||
|
monkeypatch.setattr(deps, "get_tts_provider", lambda *a, **k: FakePiperTTSProvider())
|
||||||
|
# Klassennamen auf den vom warmup geprueften Namen setzen.
|
||||||
|
FakePiperTTSProvider.__name__ = "PiperTTSProvider"
|
||||||
|
|
||||||
|
asyncio.run(warmup_local_models())
|
||||||
|
assert calls.get("tts") # synthesize wurde aufgerufen
|
||||||
Loading…
Add table
Add a link
Reference in a new issue