3611defe5d
Neues services/voice_metrics.py (rollend, thread-safe, in-memory): misst STT, Vision-Beschreibung, Chat-TTFB (Hermes-Stream) und TTS server-seitig. voice.py instrumentiert die vier Stufen; GET /api/voice/metrics liefert avg/p50/p95/last je Stufe. Macht aus Latenz-Vermutungen Messdaten — Anzeige folgt im Frontend (E). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
69 lines
2.0 KiB
Python
69 lines
2.0 KiB
Python
"""
|
|
Per-Stage-Latenz-Metriken für die Voice/Lucy-Pipeline.
|
|
|
|
Misst die Server-seitige Dauer jeder Stufe (STT, Vision-Beschreibung, Chat-TTFB, TTS) und hält
|
|
rollende Statistiken (avg/p50/p95/last) im Speicher. Macht aus Latenz-VERMUTUNGEN gemessene Fakten
|
|
— die eigentliche Voraussetzung, um gezielt zu optimieren (Stufe 5/C2 des Reviews). Anzeige im
|
|
Frontend-Overhaul (E) analog zur TokenPerformanceCard.
|
|
|
|
In-Memory + thread-safe (keine Datei-I/O — Latenz-Telemetrie ist transient, Restart = Reset).
|
|
"""
|
|
|
|
import threading
|
|
import time
|
|
from collections import deque
|
|
|
|
_LOCK = threading.Lock()
|
|
_MAX = 200
|
|
_STAGES: dict[str, deque] = {}
|
|
|
|
# Bekannte Stufen (für stabile UI-Reihenfolge); unbekannte werden trotzdem erfasst.
|
|
STAGES = ("stt", "vision", "chat_ttfb", "tts")
|
|
|
|
|
|
def record_stage(stage: str, ms: float) -> None:
|
|
"""Eine gemessene Stage-Dauer (ms) verbuchen. No-op bei negativen Werten."""
|
|
if ms is None or ms < 0:
|
|
return
|
|
with _LOCK:
|
|
dq = _STAGES.get(stage)
|
|
if dq is None:
|
|
dq = _STAGES[stage] = deque(maxlen=_MAX)
|
|
dq.append(float(ms))
|
|
|
|
|
|
class Timer:
|
|
"""Context-Manager: misst die verstrichene Zeit und verbucht sie auf `stage`.
|
|
Funktioniert um `await`-Aufrufe herum (enter → await → exit)."""
|
|
|
|
def __init__(self, stage: str) -> None:
|
|
self.stage = stage
|
|
self._t0 = 0.0
|
|
|
|
def __enter__(self) -> "Timer":
|
|
self._t0 = time.perf_counter()
|
|
return self
|
|
|
|
def __exit__(self, *exc) -> None:
|
|
record_stage(self.stage, (time.perf_counter() - self._t0) * 1000.0)
|
|
|
|
|
|
def _summary(vals: list[float]) -> dict:
|
|
if not vals:
|
|
return {"count": 0}
|
|
s = sorted(vals)
|
|
n = len(s)
|
|
return {
|
|
"count": n,
|
|
"avg_ms": round(sum(s) / n, 1),
|
|
"p50_ms": round(s[n // 2], 1),
|
|
"p95_ms": round(s[min(n - 1, int(n * 0.95))], 1),
|
|
"last_ms": round(vals[-1], 1),
|
|
}
|
|
|
|
|
|
def get_metrics() -> dict:
|
|
"""Rollende Zusammenfassung je Stufe."""
|
|
with _LOCK:
|
|
return {stage: _summary(list(dq)) for stage, dq in _STAGES.items()}
|