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:
parent
caa2883a66
commit
cf1a0ccdd1
1 changed files with 48 additions and 18 deletions
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue