99 lines
3.0 KiB
Python
99 lines
3.0 KiB
Python
import re
|
|
|
|
import httpx
|
|
|
|
from .config import MINIMAX_KEY, MINIMAX_MODEL, MINIMAX_URL
|
|
|
|
THINK_BLOCK = re.compile(r"<think>.*?</think>", re.DOTALL)
|
|
|
|
SYSTEM = (
|
|
"Du schreibst kurze deutsche Teaser für Horrorfilme. "
|
|
"Antworte immer auf Deutsch, auch wenn die Vorlage englisch ist. "
|
|
"Übersetze die Vorlage nicht, sondern formuliere neu. "
|
|
"Genau zwei Sätze, zusammen höchstens 40 Wörter. "
|
|
"Atmosphärisch und reißerisch, aber ohne Spoiler und ohne das Ende zu verraten. "
|
|
"Kein Filmtitel im Text, keine Anführungszeichen, keine Einleitung. "
|
|
"Gib nur den Teaser aus."
|
|
)
|
|
|
|
|
|
def build_prompt(movie):
|
|
year = movie.get("year") or "unbekannt"
|
|
overview = (movie.get("overview") or "").strip()
|
|
if overview:
|
|
return (
|
|
f"Film: {movie['title']} ({year})\n"
|
|
f"Inhaltsangabe: {overview}\n\n"
|
|
"Schreibe den Teaser."
|
|
)
|
|
return (
|
|
f"Film: {movie['title']} ({year})\n"
|
|
"Es liegt keine Inhaltsangabe vor. Schreibe einen passenden, "
|
|
"allgemein gehaltenen Horror-Teaser zu diesem Titel."
|
|
)
|
|
|
|
|
|
def clean(text):
|
|
text = THINK_BLOCK.sub("", text)
|
|
return " ".join(text.split()).strip('"„“ ')
|
|
|
|
|
|
def fallback(movie):
|
|
"""MiniMax nicht erreichbar: gekürzte TMDB-Synopsis."""
|
|
overview = (movie.get("overview") or "").strip()
|
|
if not overview:
|
|
return ""
|
|
sentences = re.split(r"(?<=[.!?])\s+", overview)
|
|
return " ".join(sentences[:2])
|
|
|
|
|
|
def build_payload(movie):
|
|
return {
|
|
"model": MINIMAX_MODEL,
|
|
"messages": [
|
|
{"role": "system", "content": SYSTEM},
|
|
{"role": "user", "content": build_prompt(movie)},
|
|
],
|
|
# M3 denkt sonst über das Token-Limit hinaus und liefert nur den <think>-Block
|
|
"reasoning_effort": "low",
|
|
"max_tokens": 1500,
|
|
"temperature": 0.8,
|
|
}
|
|
|
|
|
|
async def ask(http, movie, max_tokens):
|
|
payload = {**build_payload(movie), "max_tokens": max_tokens}
|
|
resp = await http.post(MINIMAX_URL, json=payload)
|
|
resp.raise_for_status()
|
|
return clean(resp.json()["choices"][0]["message"]["content"])
|
|
|
|
|
|
def complete(text):
|
|
"""Am Token-Limit bricht der Teaser mitten im Wort ab."""
|
|
return bool(text) and text.rstrip().endswith((".", "!", "?", "…", '"'))
|
|
|
|
|
|
async def generate(http, movie, semaphore):
|
|
async with semaphore:
|
|
try:
|
|
text = await ask(http, movie, 1500)
|
|
if not complete(text):
|
|
# Denkt das Modell zu lang, bleibt vom Teaser nichts oder
|
|
# nur ein angefangener Satz übrig
|
|
retry = await ask(http, movie, 4000)
|
|
if complete(retry) or not text:
|
|
text = retry
|
|
return text or fallback(movie)
|
|
except (httpx.HTTPError, KeyError, IndexError):
|
|
return fallback(movie)
|
|
|
|
|
|
def client():
|
|
return httpx.AsyncClient(
|
|
headers={
|
|
"Authorization": f"Bearer {MINIMAX_KEY}",
|
|
"Content-Type": "application/json",
|
|
},
|
|
timeout=120.0,
|
|
)
|