init
This commit is contained in:
98
backend/app/teaser.py
Normal file
98
backend/app/teaser.py
Normal file
@@ -0,0 +1,98 @@
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user