fix: qwen2.5:14b als Primärmodell, robustes JSON-Parsing

- 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
This commit is contained in:
orbitalo 2026-03-27 12:39:34 +00:00
parent caa2883a66
commit cf1a0ccdd1

View file

@ -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)