Feat: Bildschirm-Sicht Multi-Monitor — beide Screens an das Vision-Modell

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 <noreply@anthropic.com>
This commit is contained in:
Hitonabi
2026-06-28 22:31:04 +02:00
parent 9c3dd9be82
commit 68cad9c0f1
+22 -16
View File
@@ -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 = {