diff --git a/backend/routers/voice.py b/backend/routers/voice.py index 211de16..fe99a78 100644 --- a/backend/routers/voice.py +++ b/backend/routers/voice.py @@ -57,7 +57,9 @@ async def _describe_images(image_urls: list[str], hint: str) -> str: for u in image_urls: content.append({"type": "image_url", "image_url": {"url": u}}) 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={ "model": VISION_MODEL, "max_tokens": VISION_MAX_TOKENS, "stream": False, "messages": [{"role": "user", "content": content}], @@ -193,26 +195,6 @@ async def voice_chat(body: ChatIn) -> StreamingResponse: if not HERMES_API_KEY: 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 = { "Authorization": f"Bearer {HERMES_API_KEY}", "X-Hermes-Session-Id": body.session_id, @@ -224,6 +206,28 @@ async def voice_chat(body: ChatIn) -> StreamingResponse: t0 = time.perf_counter() first = 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: async with httpx.AsyncClient(timeout=httpx.Timeout(None, connect=5.0)) as client: async with client.stream( diff --git a/client/lucy-desktop/src/renderer/src/lib/voice/useVoiceAgent.ts b/client/lucy-desktop/src/renderer/src/lib/voice/useVoiceAgent.ts index 66b2e96..f9347db 100644 --- a/client/lucy-desktop/src/renderer/src/lib/voice/useVoiceAgent.ts +++ b/client/lucy-desktop/src/renderer/src/lib/voice/useVoiceAgent.ts @@ -148,8 +148,20 @@ export function useVoiceAgent() { useEffect(() => { let cancelled = false ;(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 (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 try { await tts.synthesize("Alles bereit, Commander.") } catch { /* */ } if (cancelled) return diff --git a/deploy/llama-swap.config.yaml b/deploy/llama-swap.config.yaml index d7ca508..a49e753 100644 --- a/deploy/llama-swap.config.yaml +++ b/deploy/llama-swap.config.yaml @@ -5,7 +5,10 @@ globalTTL: 0 models: Qwen3.6-35B-A3B: 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 aliases: - hermes diff --git a/docs/REVIEW_2026-07-02.md b/docs/REVIEW_2026-07-02.md index d3f61fe..e0e56a4 100644 --- a/docs/REVIEW_2026-07-02.md +++ b/docs/REVIEW_2026-07-02.md @@ -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 | | C3 | 🟢 | Thinking-Disable 3× dupliziert | voice.py / gateway.py / 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 | | 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 | @@ -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. +**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.*