From 9c3dd9be82164ba61aaa16e40ad33b885c3eaa15 Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 28 Jun 2026 22:26:55 +0200 Subject: [PATCH] Feat: Bildschirm-Sicht nutzt das dedizierte Vision-Modell (Qwen3-VL-8B) Statt das Bild an die fast-MoE (schwaechere Vision) zu geben: das VL-Modell beschreibt den Screenshot, die Beschreibung geht als Text-Kontext an Hermes. -> bessere Bilderkennung UND Lucy behaelt ihr volles Hirn/Gedaechtnis. MC_VISION_MODEL (default 'vision') steuerbar. Co-Authored-By: Claude Opus 4.8 --- backend/routers/voice.py | 43 +++++++++++++++++++++++++++++++--------- 1 file changed, 34 insertions(+), 9 deletions(-) diff --git a/backend/routers/voice.py b/backend/routers/voice.py index 63eeed6..723ad4b 100644 --- a/backend/routers/voice.py +++ b/backend/routers/voice.py @@ -10,17 +10,43 @@ LAN-only (kein Token in der 2.0-Phase), wie die übrigen MC2-Endpoints. """ import logging +import os import httpx from fastapi import APIRouter, File, Form, HTTPException, UploadFile from fastapi.responses import Response, StreamingResponse from pydantic import BaseModel -from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, VOICE_SERVICE_URL +from config import HERMES_API_KEY, HERMES_API_MODEL, HERMES_API_URL, LLAMA_SWAP_URL, VOICE_SERVICE_URL log = logging.getLogger(__name__) router = APIRouter(prefix="/api") +# Bildschirm-Sicht: das DEDIZIERTE Vision-Modell (Qwen3-VL-8B) beschreibt das Bild; die Beschreibung +# geht als TEXT an Hermes -> Lucy behält ihr volles Hirn/Gedächtnis UND nutzt das bessere VL-Modell +# (statt der schwächeren Vision der fast-MoE). Per Env abschaltbar/umstellbar. +VISION_MODEL = os.environ.get("MC_VISION_MODEL", "vision") + + +async def _describe_image(image_url: str, hint: str) -> str: + """Lässt das Vision-Modell den Screenshot knapp beschreiben (Deutsch). Leerer String bei Fehler.""" + prompt = ("Beschreibe knapp und präzise auf Deutsch, was auf diesem Screenshot zu sehen ist " + "(App/Fenster, wichtige Inhalte, sichtbarer Text/Code). Frage des Nutzers dazu: " + hint) + try: + async with httpx.AsyncClient(timeout=httpx.Timeout(90.0, connect=5.0)) as client: + r = await client.post(f"{LLAMA_SWAP_URL}/v1/chat/completions", json={ + "model": VISION_MODEL, "max_tokens": 450, "stream": False, + "messages": [{"role": "user", "content": [ + {"type": "text", "text": prompt}, + {"type": "image_url", "image_url": {"url": image_url}}, + ]}], + }) + r.raise_for_status() + return (r.json().get("choices") or [{}])[0].get("message", {}).get("content", "").strip() + except Exception as exc: + log.warning("Vision-Beschreibung fehlgeschlagen: %s", exc) + return "" + _TIMEOUT = httpx.Timeout(120.0, connect=5.0) # Chatterbox-TTS auf CPU darf dauern @@ -140,15 +166,14 @@ async def voice_chat(body: ChatIn) -> StreamingResponse: messages = [] if body.system: messages.append({"role": "system", "content": body.system}) + user_text = body.text if body.image: - # Bildschirm-Sicht: User-Message als multimodal (Text + Bild). Das Agent-Modell (fast/Qwen3.6 - # mit mmproj) bzw. ein Vision-Modell verarbeitet das Bild via OpenAI-image_url. - messages.append({"role": "user", "content": [ - {"type": "text", "text": body.text}, - {"type": "image_url", "image_url": {"url": body.image}}, - ]}) - else: - messages.append({"role": "user", "content": body.text}) + # Bildschirm-Sicht: erst das Vision-Modell beschreiben lassen, dann die Beschreibung als TEXT- + # Kontext an Hermes (Lucy antwortet mit vollem Hirn/Gedächtnis, sieht aber via besserem VL-Modell). + desc = await _describe_image(body.image, body.text) + if desc: + user_text = f"[Bildschirm-Sicht — das ist gerade auf dem Schirm zu sehen:\n{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} headers = { "Authorization": f"Bearer {HERMES_API_KEY}",