This commit is contained in:
team3
2026-09-18 23:47:25 +02:00
parent db77c1f985
commit a90044b0a7
21 changed files with 1111 additions and 253 deletions

View File

@@ -18,22 +18,23 @@ from .config import LANGUAGE, MINIMAX_KEY, MINIMAX_MODEL, MINIMAX_URL
from .teaser import THINK_BLOCK
ANZAHL = 12
MAX_ANZAHL = 100
MAX_BESCHREIBUNG = 1000
SYSTEM = (
"Du bist ein belesener Filmkenner mit Schwerpunkt Horror und Genrekino. "
"Du schlägst Filme vor, die zu dem passen, was gesucht wird. "
f"Nenne genau {ANZAHL} Filme, keine Serien. Nur Filme, die es wirklich gibt. "
"Nenne genau {anzahl} Filme, keine Serien. Nur Filme, die es wirklich gibt. "
"Nichts vorschlagen, was in der Sammlung steht oder schon gesehen wurde. "
"Was in der Frage genannt ist, kennt die Person schon — auch nicht vorschlagen. "
"Mische Bekanntes mit Geheimtipps. "
"Antworte nur mit JSON in dieser Form, ohne Text davor oder danach: "
'{"merkmale": ["drei Stichpunkte: was macht das Gesuchte aus"], '
'"kandidaten": ["20 Originaltitel mit Jahr, die dazu passen könnten"], '
'"filme": [{"titel": "Originaltitel", "jahr": 1999, "grund": "..."}]}\n'
'{{"merkmale": ["drei Stichpunkte: was macht das Gesuchte aus"], '
'"kandidaten": ["{kandidaten} Originaltitel mit Jahr, die dazu passen könnten"], '
'"filme": [{{"titel": "Originaltitel", "jahr": 1999, "grund": "..."}}]}}\n'
# Der Umweg über Merkmale und Kandidaten ersetzt das Nachdenken des
# Modells: es sortiert erst, dann wählt es — und trifft deutlich besser.
"Erst merkmale, dann kandidaten, dann die zwölf besten davon als filme.\n"
"Erst merkmale, dann kandidaten, dann die {anzahl} besten davon als filme.\n"
# Die Begründung lesen Leute, die einfach einen Film für den Abend
# suchen — nicht Kritiker. Sie muss auf vier kurze Zeilen passen.
"Für grund gilt: Höchstens 10 Wörter und 90 Zeichen, Deutsch in lateinischer "
@@ -65,6 +66,12 @@ def kuerzen(grund):
return kopf[:schnitt].rstrip(",; –.") + " …"
def system(anzahl):
"""Der Systemprompt für eine gewünschte Anzahl Filme. Die Kandidaten-
liste bleibt ein Stück länger als das Ergebnis."""
return SYSTEM.format(anzahl=anzahl, kandidaten=max(20, anzahl + 8))
def etikett(film):
"""Titel (Jahr) — so kommt ein Film in den Prompt."""
jahr = film.get("year")
@@ -124,7 +131,7 @@ def prompt(beschreibung, sammlung, gesehen, filter_=None):
return "\n\n".join(teile)
def parsen(text):
def parsen(text, anzahl=ANZAHL):
"""Das JSON aus der Antwort ziehen. Das Modell hängt gern einen
Denkblock oder einen Satz davor — es zählt, was zwischen der ersten
und der letzten Klammer steht."""
@@ -152,14 +159,14 @@ def parsen(text):
"grund": kuerzen(str(f.get("grund") or "")),
}
)
return aus[:ANZAHL]
return aus[:anzahl]
async def fragen(beschreibung, sammlung, gesehen, filter_=None):
async def fragen(beschreibung, sammlung, gesehen, filter_=None, anzahl=ANZAHL):
payload = {
"model": MINIMAX_MODEL,
"messages": [
{"role": "system", "content": SYSTEM},
{"role": "system", "content": system(anzahl)},
{"role": "user", "content": prompt(beschreibung, sammlung, gesehen, filter_)},
],
# Ohne Denkblock: M3 grübelt sonst minutenlang über zwölf Filme
@@ -167,7 +174,8 @@ async def fragen(beschreibung, sammlung, gesehen, filter_=None):
# Kandidatenfelder im Prompt leisten das Sortieren stattdessen,
# in gut zehn Sekunden. Niedrige Temperatur: weniger Ausreißer.
"thinking": {"type": "disabled"},
"max_tokens": 4000,
# Rund 40 Token je Film, dazu Merkmale und Kandidaten
"max_tokens": 2000 + 60 * anzahl,
"temperature": 0.3,
}
async with httpx.AsyncClient(
@@ -178,7 +186,7 @@ async def fragen(beschreibung, sammlung, gesehen, filter_=None):
for _ in range(2):
resp = await http.post(MINIMAX_URL, json=payload)
resp.raise_for_status()
filme = parsen(resp.json()["choices"][0]["message"]["content"])
filme = parsen(resp.json()["choices"][0]["message"]["content"], anzahl)
if filme:
return filme
return []