From 68cad9c0f1e6571b00191d5cf93e6bb0061d250d Mon Sep 17 00:00:00 2001 From: Hitonabi Date: Sun, 28 Jun 2026 22:31:04 +0200 Subject: [PATCH] =?UTF-8?q?Feat:=20Bildschirm-Sicht=20Multi-Monitor=20?= =?UTF-8?q?=E2=80=94=20beide=20Screens=20an=20das=20Vision-Modell?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ChatIn.images (Liste, 1 data-URL je Monitor); _describe_images schickt alle Screenshots in EINER Nachricht ans VL-Modell -> Lucy sieht beide Bildschirme. Co-Authored-By: Claude Opus 4.8 --- backend/routers/voice.py | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/backend/routers/voice.py b/backend/routers/voice.py index 723ad4b..1d1acc7 100644 --- a/backend/routers/voice.py +++ b/backend/routers/voice.py @@ -28,18 +28,23 @@ router = APIRouter(prefix="/api") 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) +async def _describe_images(image_urls: list[str], hint: str) -> str: + """Lässt das Vision-Modell die Screenshots (1 je Monitor) knapp beschreiben (Deutsch). + Mehrere Bilder gehen in EINER Nachricht ans VL-Modell. Leerer String bei Fehler.""" + multi = len(image_urls) > 1 + intro = (f"Hier sind {len(image_urls)} Screenshots (je ein Monitor). Beschreibe knapp und präzise auf Deutsch, " + "was auf den Bildschirmen zu sehen ist (pro Monitor: App/Fenster, wichtige Inhalte, sichtbarer Text/Code). " + if multi else + "Beschreibe knapp und präzise auf Deutsch, was auf diesem Screenshot zu sehen ist " + "(App/Fenster, wichtige Inhalte, sichtbarer Text/Code). ") + content: list = [{"type": "text", "text": intro + "Frage des Nutzers dazu: " + hint}] + for u in image_urls: + content.append({"type": "image_url", "image_url": {"url": u}}) try: - async with httpx.AsyncClient(timeout=httpx.Timeout(90.0, connect=5.0)) as client: + async with httpx.AsyncClient(timeout=httpx.Timeout(120.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}}, - ]}], + "model": VISION_MODEL, "max_tokens": 600, "stream": False, + "messages": [{"role": "user", "content": content}], }) r.raise_for_status() return (r.json().get("choices") or [{}])[0].get("message", {}).get("content", "").strip() @@ -64,7 +69,7 @@ class ChatIn(BaseModel): session_key: str = "" # optional: Langzeit-Memory-Scope system: str = "" # optionaler ephemerer System-Prompt (z.B. „antworte knapp/gesprochen") model: str = "" - image: str = "" # optionale Bildschirm-Sicht: Bild als data:-URL (Lucys „Augen") + images: list[str] = [] # optionale Bildschirm-Sicht: ein data:-URL je Monitor (Lucys „Augen") @router.get("/voice/health") @@ -167,12 +172,13 @@ async def voice_chat(body: ChatIn) -> StreamingResponse: if body.system: messages.append({"role": "system", "content": body.system}) user_text = body.text - if body.image: - # 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) + 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). + desc = await _describe_images(imgs, body.text) if desc: - user_text = f"[Bildschirm-Sicht — das ist gerade auf dem Schirm zu sehen:\n{desc}\n]\n\n{body.text}" + user_text = f"[Bildschirm-Sicht — das ist gerade auf dem/den Schirm(en) 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 = {