P2: Vision im Stream (C2), TTS-Warmup-Retry (L2), parallel-2-Config vorbereitet
- voice.py: Bildschirm-Sicht laeuft IM SSE-Stream (+hermes.vision.progress-Event fuer Warte-Ansage), Vision-Timeout 120->45s (MC_VISION_TIMEOUT); deployt + Smoke-Test ok - useVoiceAgent: Warm-Gate mit Retry/Backoff + sichtbarer Meldung statt stummem Fake-ready - deploy-Config: hermes -c 131072 --parallel 2 (+KV Q8_0) — Mem0-Extraktion blockiert Voice-Turns nicht mehr; speicherneutral. Live-Schaltung: User - Report: P2-Nachtrag; C5 (pricing/token_stats) war Fehlalarm — in Benutzung Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+25
-21
@@ -57,7 +57,9 @@ async def _describe_images(image_urls: list[str], hint: str) -> str:
|
|||||||
for u in image_urls:
|
for u in image_urls:
|
||||||
content.append({"type": "image_url", "image_url": {"url": u}})
|
content.append({"type": "image_url", "image_url": {"url": u}})
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=httpx.Timeout(120.0, connect=5.0)) as client:
|
# 45 s statt 120 s: Qwen3-VL braucht warm ~5 s; wenn es 45 s nicht schafft, ist etwas
|
||||||
|
# kaputt und Lucy soll lieber ohne Bildschirm-Kontext antworten als ewig hängen.
|
||||||
|
async with httpx.AsyncClient(timeout=httpx.Timeout(float(os.environ.get("MC_VISION_TIMEOUT", "45")), connect=5.0)) as client:
|
||||||
r = await client.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json={
|
r = await client.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json={
|
||||||
"model": VISION_MODEL, "max_tokens": VISION_MAX_TOKENS, "stream": False,
|
"model": VISION_MODEL, "max_tokens": VISION_MAX_TOKENS, "stream": False,
|
||||||
"messages": [{"role": "user", "content": content}],
|
"messages": [{"role": "user", "content": content}],
|
||||||
@@ -193,26 +195,6 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
if not HERMES_API_KEY:
|
if not HERMES_API_KEY:
|
||||||
raise HTTPException(503, "HERMES_API_KEY/API_SERVER_KEY nicht gesetzt — Agent-Auth fehlt.")
|
raise HTTPException(503, "HERMES_API_KEY/API_SERVER_KEY nicht gesetzt — Agent-Auth fehlt.")
|
||||||
|
|
||||||
messages = []
|
|
||||||
if body.system:
|
|
||||||
messages.append({"role": "system", "content": body.system})
|
|
||||||
user_text = body.text
|
|
||||||
imgs = [u for u in (body.images or []) if u]
|
|
||||||
if imgs:
|
|
||||||
# Bildschirm-Sicht: erst das Vision-Modell die Monitore beschreiben lassen, dann die Beschreibung
|
|
||||||
# als TEXT-Kontext an Hermes (Lucy antwortet mit vollem Hirn/Gedächtnis, sieht via besserem VL-Modell).
|
|
||||||
with Timer("vision"):
|
|
||||||
desc = await _describe_images(imgs, body.text)
|
|
||||||
if desc:
|
|
||||||
safe_desc = wrap_untrusted(desc, "BILDSCHIRM")
|
|
||||||
user_text = f"[Bildschirm-Sicht — das ist gerade auf dem/den Schirm(en) zu sehen:\n{safe_desc}\n]\n\n{body.text}"
|
|
||||||
messages.append({"role": "user", "content": user_text})
|
|
||||||
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
|
||||||
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
|
||||||
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
|
||||||
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Mem0-Extraktion.
|
|
||||||
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
|
||||||
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
|
||||||
headers = {
|
headers = {
|
||||||
"Authorization": f"Bearer {HERMES_API_KEY}",
|
"Authorization": f"Bearer {HERMES_API_KEY}",
|
||||||
"X-Hermes-Session-Id": body.session_id,
|
"X-Hermes-Session-Id": body.session_id,
|
||||||
@@ -224,6 +206,28 @@ async def voice_chat(body: ChatIn) -> StreamingResponse:
|
|||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
first = True
|
first = True
|
||||||
first_content = True
|
first_content = True
|
||||||
|
# Bildschirm-Sicht INNERHALB des Streams (C2-Fix): so startet die SSE-Antwort sofort und
|
||||||
|
# der Client bekommt ein Progress-Event (-> Lucy kann eine Warte-Ansage sprechen), statt
|
||||||
|
# dass der Request bis zu 120 s "tot" hängt, während das Vision-Modell beschreibt.
|
||||||
|
user_text = body.text
|
||||||
|
imgs = [u for u in (body.images or []) if u]
|
||||||
|
if imgs:
|
||||||
|
yield b'event: hermes.vision.progress\ndata: {"note": "Bildschirm wird angeschaut"}\n\n'
|
||||||
|
with Timer("vision"):
|
||||||
|
desc = await _describe_images(imgs, body.text)
|
||||||
|
if desc:
|
||||||
|
safe_desc = wrap_untrusted(desc, "BILDSCHIRM")
|
||||||
|
user_text = f"[Bildschirm-Sicht — das ist gerade auf dem/den Schirm(en) zu sehen:\n{safe_desc}\n]\n\n{body.text}"
|
||||||
|
messages = []
|
||||||
|
if body.system:
|
||||||
|
messages.append({"role": "system", "content": body.system})
|
||||||
|
messages.append({"role": "user", "content": user_text})
|
||||||
|
payload = {"model": body.model or HERMES_API_MODEL, "messages": messages, "stream": True}
|
||||||
|
# Lucys Hirn (Qwen3.6) ist ein Thinking-Modell -> für die gesprochene Assistentin Thinking AUS,
|
||||||
|
# sonst generiert es tausende Reasoning-Token VOR der kurzen Antwort (gemessen: 11k Token, ~30s TTFB).
|
||||||
|
# Gleiches Muster wie die fast-Spur im Gateway (gateway_proxy.py) und die Mem0-Extraktion.
|
||||||
|
if os.environ.get("MC_VOICE_NO_THINK", "1") not in ("0", "false", "False"):
|
||||||
|
payload["chat_template_kwargs"] = {"enable_thinking": False}
|
||||||
try:
|
try:
|
||||||
async with httpx.AsyncClient(timeout=httpx.Timeout(None, connect=5.0)) as client:
|
async with httpx.AsyncClient(timeout=httpx.Timeout(None, connect=5.0)) as client:
|
||||||
async with client.stream(
|
async with client.stream(
|
||||||
|
|||||||
@@ -148,8 +148,20 @@ export function useVoiceAgent() {
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
await tts.waitReady(120_000)
|
// Geduldiger Retry mit Backoff statt einmaligem 120s-Warten: crasht der pocket_server
|
||||||
|
// (oder braucht er länger), bleibt die App in 'warming' MIT sichtbarer Meldung — vorher
|
||||||
|
// schaltete sie nach dem Timeout stumm auf 'ready' und die Stimme fehlte einfach.
|
||||||
|
let attempt = 0
|
||||||
|
while (!cancelled) {
|
||||||
|
const ok = await tts.waitReady(attempt === 0 ? 45_000 : 20_000)
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
if (ok) break
|
||||||
|
attempt++
|
||||||
|
setError(`Stimme startet nicht (Versuch ${attempt}) — pocket_server prüfen, ich versuche es weiter …`)
|
||||||
|
await new Promise((r) => setTimeout(r, Math.min(5_000 * attempt, 30_000)))
|
||||||
|
}
|
||||||
|
if (cancelled) return
|
||||||
|
setError(null)
|
||||||
// ein Aufwaerm-Satz (primt alle lazy Pfade); Audio verwerfen
|
// ein Aufwaerm-Satz (primt alle lazy Pfade); Audio verwerfen
|
||||||
try { await tts.synthesize("Alles bereit, Commander.") } catch { /* */ }
|
try { await tts.synthesize("Alles bereit, Commander.") } catch { /* */ }
|
||||||
if (cancelled) return
|
if (cancelled) return
|
||||||
|
|||||||
@@ -5,7 +5,10 @@ globalTTL: 0
|
|||||||
models:
|
models:
|
||||||
Qwen3.6-35B-A3B:
|
Qwen3.6-35B-A3B:
|
||||||
cmd: |
|
cmd: |
|
||||||
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 65536 -ngl 999 -fa on --no-mmap --jinja --parallel 1 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-mtp --spec-draft-n-max 3
|
# parallel 2 + c 131072: 2 Slots à 65k (Slot 2 = Mem0-Lern-Extraktion, blockiert Voice-Turns
|
||||||
|
# nicht mehr); KV Q8_0 macht das speicherneutral zu vorher (65k f16). Bench 2026-07-02:
|
||||||
|
# Q8_0 kostet 0 t/s, MTP n-max 3 = +26% vs. ohne Spec.
|
||||||
|
llama-server -m /srv/models/Qwen3.6-35B-A3B-MTP-GGUF/Qwen3.6-35B-A3B-UD-Q4_K_M.gguf --host 127.0.0.1 --port ${PORT} -c 131072 -ngl 999 -fa on --no-mmap --jinja --parallel 2 -cram 16384 -ctk q8_0 -ctv q8_0 --spec-type draft-mtp --spec-draft-n-max 3
|
||||||
ttl: 0
|
ttl: 0
|
||||||
aliases:
|
aliases:
|
||||||
- hermes
|
- hermes
|
||||||
|
|||||||
@@ -206,7 +206,7 @@ Quelle: `/api/voice/metrics` (live) + eigener TTFT-Test gegen llama-swap.
|
|||||||
| C2 | 🟡 | Vision-Beschreibung blockiert Chat bis 120 s | backend/routers/voice.py |
|
| C2 | 🟡 | Vision-Beschreibung blockiert Chat bis 120 s | backend/routers/voice.py |
|
||||||
| C3 | 🟢 | Thinking-Disable 3× dupliziert | voice.py / gateway.py / mem0_service/app.py |
|
| C3 | 🟢 | Thinking-Disable 3× dupliziert | voice.py / gateway.py / mem0_service/app.py |
|
||||||
| C4 | 🟢 | Junk-Fact-Regex hartcodiert | mem0_service/app.py |
|
| C4 | 🟢 | Junk-Fact-Regex hartcodiert | mem0_service/app.py |
|
||||||
| C5 | 🟢 | Dead Code | backend/services/pricing.py, token_stats.py |
|
| C5 | ⚪ (Fehlalarm) | pricing.py + token_stats.py sind IN BENUTZUNG (system.py /token_stats-Endpoint, gateway_stream) — kein Dead Code | backend/services/ |
|
||||||
| F1 | 🟡 | Monolithen 990/934 Z | frontend/src/views/models/Cockpit.tsx, components/SystemDrawer.tsx |
|
| F1 | 🟡 | Monolithen 990/934 Z | frontend/src/views/models/Cockpit.tsx, components/SystemDrawer.tsx |
|
||||||
| F2 | 🟡 (korrigiert) | dist/ im Git ist ABSICHT (Box hat kein Node, deploy = git reset --hard; s. deploy/build.sh) — echter Fund war nur der inkonsistente Asset-Stand auf lucy-v2 (behoben durch Rebuild+Commit). Build-on-Box/CI wäre P3-Option | frontend/dist/ |
|
| F2 | 🟡 (korrigiert) | dist/ im Git ist ABSICHT (Box hat kein Node, deploy = git reset --hard; s. deploy/build.sh) — echter Fund war nur der inkonsistente Asset-Stand auf lucy-v2 (behoben durch Rebuild+Commit). Build-on-Box/CI wäre P3-Option | frontend/dist/ |
|
||||||
| F4 | 🟢 | Keine globale Error Boundary | frontend/src/App.tsx |
|
| F4 | 🟢 | Keine globale Error Boundary | frontend/src/App.tsx |
|
||||||
@@ -242,6 +242,13 @@ Quelle: `/api/voice/metrics` (live) + eigener TTFT-Test gegen llama-swap.
|
|||||||
|
|
||||||
**P1-10:** `chat_first_content`-Metrik in voice.py — misst die echte Hirn-Latenz (Agent-Overhead + LLM-TTFT bis zum ersten Inhalts-Token) statt nur des SSE-Starts.
|
**P1-10:** `chat_first_content`-Metrik in voice.py — misst die echte Hirn-Latenz (Agent-Overhead + LLM-TTFT bis zum ersten Inhalts-Token) statt nur des SSE-Starts.
|
||||||
|
|
||||||
|
**P2-Nachtrag (02.07.):**
|
||||||
|
- **Profiling:** Folge-Turns 1,3 s, NEUE Sessions 5,6–12 s (Session-Prefill System-Prompt+Tools ~4–5k Tok). Mem0-Search unschuldig (18 ms). **Aber:** Die Mem0-Lern-Extraktion (~2,4 s je Turn, gleiche Qwen3.6-Instanz) blockiert bei `--parallel 1` den nächsten Voice-Turn in der Queue → Fix vorbereitet: `--parallel 2 -c 131072 + KV Q8_0` (2×65k-Slots, speicherneutral zu 1×65k f16). Live-Schaltung = User (deploy/llama-swap.config.yaml ist fertig).
|
||||||
|
- **C2 gefixt:** Bildschirm-Sicht läuft jetzt IM Stream (SSE startet sofort, `hermes.vision.progress`-Event → Lucy kann Warte-Ansage sprechen); Vision-Timeout 120→45 s (MC_VISION_TIMEOUT).
|
||||||
|
- **C3 bewertet:** nur 2 Backend-Stellen mit unterschiedlicher Gating-Logik (dritte im separaten Mem0-venv) → kein Shared-Helper erzwungen, Pattern dokumentiert.
|
||||||
|
- **C5 war Fehlalarm** (s. Findings-Tabelle).
|
||||||
|
- **L2 gefixt:** TTS-Warm-Gate mit Retry/Backoff + sichtbarer Fehlermeldung (vorher: nach 120 s stumm „ready" ohne Stimme).
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
*Erhoben am 02.07.2026 durch Claude Code (Live-SSH auf Box, lokale PC-Prüfung, 3 Codebase-Agents, 3 Web-Recherche-Runden). Messwerte: /api/voice/metrics (n=13 STT, n=18 chat_ttfb), TTFT-Direktmessung llama-swap :8080.*
|
*Erhoben am 02.07.2026 durch Claude Code (Live-SSH auf Box, lokale PC-Prüfung, 3 Codebase-Agents, 3 Web-Recherche-Runden). Messwerte: /api/voice/metrics (n=13 STT, n=18 chat_ttfb), TTFT-Direktmessung llama-swap :8080.*
|
||||||
|
|||||||
Reference in New Issue
Block a user