From cf1a0ccdd1c72b44eafbd52972164b8b0def964e Mon Sep 17 00:00:00 2001 From: orbitalo Date: Fri, 27 Mar 2026 12:39:34 +0000 Subject: [PATCH] =?UTF-8?q?fix:=20qwen2.5:14b=20als=20Prim=C3=A4rmodell,?= =?UTF-8?q?=20robustes=20JSON-Parsing?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - qwen3 schreibt Reasoning in Content trotz think=false - qwen2.5 liefert zuverlässig JSON - Actors-Feld: Strings oder Objekte werden beides akzeptiert - Prompt explizit auf Deutsch eingeschränkt --- homelab-ai-bot/savetv_enrich.py | 66 ++++++++++++++++++++++++--------- 1 file changed, 48 insertions(+), 18 deletions(-) diff --git a/homelab-ai-bot/savetv_enrich.py b/homelab-ai-bot/savetv_enrich.py index 5a37103f..2e590d59 100644 --- a/homelab-ai-bot/savetv_enrich.py +++ b/homelab-ai-bot/savetv_enrich.py @@ -31,8 +31,8 @@ logging.basicConfig( ) OLLAMA_BASE = "http://100.84.255.83:11434" -MODEL = "qwen3:30b-a3b" -FALLBACK_MODEL = "qwen2.5:14b" +MODEL = "qwen2.5:14b" +FALLBACK_MODEL = "qwen3:30b-a3b" FILMINFO_CACHE = Path("/mnt/savetv/.filminfo_cache.json") BATCH_SIZE = 8 @@ -58,13 +58,13 @@ def _is_enriched(entry: dict) -> bool: def _call_ollama(prompt: str, model: str = MODEL) -> str: - """Ruft Ollama via native /api/chat auf (kein OpenAI-compat).""" + """Ruft Ollama via native /api/chat auf.""" payload = { "model": model, "messages": [ {"role": "system", "content": ( - "Du bist eine Filmdatenbank. Antworte NUR mit validem JSON, " - "kein Markdown, keine Erklärungen." + "Du bist eine Filmdatenbank. Antworte AUSSCHLIESSLICH mit validem JSON. " + "Kein Markdown, keine Erklärungen, kein Denken. Nur JSON. Sprache: Deutsch." )}, {"role": "user", "content": prompt}, ], @@ -94,21 +94,37 @@ def _call_ollama(prompt: str, model: str = MODEL) -> str: return "" +def _normalize_actors(actors_raw) -> list: + """Wandelt actors-Feld in eine einfache String-Liste um.""" + if not actors_raw or not isinstance(actors_raw, list): + return [] + result = [] + for a in actors_raw[:4]: + if isinstance(a, str): + result.append(a) + elif isinstance(a, dict): + name = a.get("name") or a.get("actor") or "" + if name: + result.append(name) + return result + + def _enrich_film(title: str) -> dict: """Fragt die KI nach Filmdaten zu einem Titel.""" clean_title = re.sub(r"\s*[-\u2013\u2014]\s*.+$", "", title).strip() prompt = f"""Gib mir Informationen zum Film "{clean_title}". -Antworte als JSON mit exakt diesen Feldern: +Antworte als JSON mit exakt diesen Feldern (alle Texte auf Deutsch): {{ - "year": "Erscheinungsjahr als String oder leer", - "countries": ["Produktionsland/länder"], - "genres": ["bis zu 3 Genres"], - "actors": ["bis zu 4 Hauptdarsteller"], - "director": "Regisseur oder leer", - "description": "3-5 Sätze auf Deutsch: Worum geht es, was macht den Film besonders, für wen ist er geeignet. Keine Spoiler." + "year": "Erscheinungsjahr als String", + "countries": ["Produktionsländer als Strings"], + "genres": ["bis zu 3 Genres als Strings"], + "actors": ["bis zu 4 Hauptdarsteller als Strings"], + "director": "Regisseur als String", + "description": "3-5 Sätze auf Deutsch: Worum geht es, für wen geeignet. Keine Spoiler." }} -Falls du den Film nicht kennst, setze description auf leer und die anderen Felder soweit bekannt.""" +Wichtig: Alle Werte müssen Strings sein. Schreibe die description komplett auf Deutsch. +Falls du den Film nicht kennst, setze description auf leeren String.""" raw = _call_ollama(prompt) if not raw: @@ -123,23 +139,36 @@ Falls du den Film nicht kennst, setze description auf leer und die anderen Felde try: data = json.loads(match.group()) except json.JSONDecodeError: - log.warning("JSON-Parse fehlgeschlagen für '%s'", title) + log.warning("JSON-Parse fehlgeschlagen für '%s': %s", title, raw[:100]) return {"year": "", "countries": [], "genres": [], "actors": [], "director": "", "description": ""} else: + log.warning("Kein JSON gefunden für '%s': %s", title, raw[:100]) return {"year": "", "countries": [], "genres": [], "actors": [], "director": "", "description": ""} + desc = str(data.get("description", ""))[:600] + if not _is_mostly_latin(desc): + desc = "" + return { "year": str(data.get("year", ""))[:4], - "countries": (data.get("countries") or [])[:3], - "genres": (data.get("genres") or [])[:3], - "actors": (data.get("actors") or [])[:4], + "countries": [str(c) for c in (data.get("countries") or [])[:3]], + "genres": [str(g) for g in (data.get("genres") or [])[:3]], + "actors": _normalize_actors(data.get("actors")), "director": str(data.get("director", ""))[:60], - "description": str(data.get("description", ""))[:600], + "description": desc, } +def _is_mostly_latin(text: str) -> bool: + """Prüft ob ein Text hauptsächlich lateinische Zeichen enthält.""" + if not text: + return False + latin = sum(1 for c in text if c.isascii() or '\u00C0' <= c <= '\u024F') + return latin / max(len(text), 1) > 0.7 + + def run(): """Hauptfunktion: Archiv laden, fehlende Filme anreichern.""" log.info("Starte Film-Enrichment...") @@ -175,6 +204,7 @@ def run(): if info.get("description"): cache[title] = info enriched += 1 + log.info(" OK: %s (%s)", info.get("year", "?"), ", ".join(info.get("actors", [])[:2])) if enriched % BATCH_SIZE == 0: _save_cache(cache) log.info(" Cache gespeichert (%d angereichert)", enriched)