diff --git a/article_server.py b/article_server.py index d25db9b..0a20198 100644 --- a/article_server.py +++ b/article_server.py @@ -1903,11 +1903,11 @@ textarea{{min-height:100px;resize:vertical}}

Review Builder

Word-Datei + Bilder hochladen. Inhalte werden automatisch zugeordnet.

-

1. Word-Datei hochladen

-
-
Word-Datei hier ablegen oder klicken (.docx)
- -
+

1. Excel-Datei hochladen

+
+
Bewertungs-Excel hier ablegen oder klicken (.xlsx)
+ +

2. Bilder hochladen

Bilder hier ablegen oder klicken
@@ -1939,23 +1939,23 @@ textarea{{min-height:100px;resize:vertical}}
""" @@ -1979,7 +1979,7 @@ document.getElementById('publishBtn').addEventListener('click',function(){{submi ) def _handle_review_builder_build(self): - """POST: Word parsen, LLM-strukturieren, HTML bauen.""" + """POST: Excel parsen, HTML bauen.""" ctype = self.headers.get("Content-Type", "") if "multipart/form-data" not in ctype: self._json_reply(400, {"ok": False, "error": "Nur multipart/form-data"}) @@ -1990,68 +1990,91 @@ document.getElementById('publishBtn').addEventListener('click',function(){{submi if boundary.startswith('"') and boundary.endswith('"'): boundary = boundary[1:-1] parts = _parse_multipart(raw, boundary) - word_data = parts.get("word") + excel_data = parts.get("excel") image_datas = [v for k, v in parts.items() if k.startswith("images_")] - meta = {k: v for k, v in parts.items() if k not in ("word",) and not k.startswith("images_")} - if not word_data: - self._json_reply(400, {"ok": False, "error": "Keine Word-Datei"}) + meta = {k: v for k, v in parts.items() if k not in ("excel",) and not k.startswith("images_")} + if not excel_data: + self._json_reply(400, {"ok": False, "error": "Keine Excel-Datei"}) return try: - from docx import Document - doc = Document(io.BytesIO(word_data)) - word_text = "\n".join(p.text for p in doc.paragraphs if p.text.strip()) + import openpyxl + wb = openpyxl.load_workbook(io.BytesIO(excel_data), data_only=True) + ws = wb["EINGABE"] + xl = {} + for row in ws.iter_rows(min_row=1, values_only=True): + key = str(row[0]).strip() if row[0] else "" + val = str(row[1]).strip() if row[1] else "" + if key and val: + xl[key] = val except Exception as e: - self._json_reply(400, {"ok": False, "error": f"Word-Fehler: {e}"}) + self._json_reply(400, {"ok": False, "error": f"Excel-Fehler: {e}"}) return - spielname = meta.get("spielname", "Unbekannt").strip() + + spielname = xl.get("Spieltitel", meta.get("spielname", "Unbekannt")).strip() + zusatz = xl.get("Zusatztitel", "").strip() + tester = xl.get("Test von", "").strip() + + # Autor-Mapping: "Jan Maguna" → autor_key autor_key = meta.get("autor", "basti").strip() + cfg = REVIEW_CONFIG + if tester and autor_key == "basti": + for k, v in cfg.get("authors", {}).items(): + if v["name"].lower() == tester.lower(): + autor_key = k + break + author_info = cfg.get("authors", {}).get(autor_key, {"name": tester or autor_key, "banner": "sebastian_meine_meinung_uberschrift.png"}) + rubrik_id = int(meta.get("rubrik", "8")) - komplex = meta.get("komplexitaet", "").strip() - disclosure_id = meta.get("disclosure", "verlag_gestellt").strip() - verlag_name = meta.get("verlag", "").strip() + do_publish = meta.get("publish", "0") == "1" fantasywelt_url = meta.get("fantasywelt_url", "").strip() so_url = meta.get("so_url", "").strip() - do_publish = meta.get("publish", "0") == "1" - cfg = REVIEW_CONFIG - author_info = cfg.get("authors", {}).get(autor_key, {"name": autor_key, "banner": "sebastian_meine_meinung_uberschrift.png"}) + + # Excel-Felder + komplex = xl.get("Komplexität des Spiels", "").strip() + autoren = xl.get("Autor:innen", "").strip() + illustratoren = xl.get("Illustrator:innen", "").strip() + verlag = xl.get("Verlag/Vertrieb", "").strip() + personen = xl.get("Personenzahl", "").strip() + spielzeit = xl.get("Spielzeit", "").strip() + jahr = xl.get("Erscheinungsjahr", "").strip() + preis = xl.get("UVP/Ø-Preis", "").strip() + app = xl.get("App-Unterstützung", "").strip() + alter = xl.get("empfohlenes Alter", "").strip() + wertung = xl.get("WERTUNG (1-100)", "").strip() + sprache = xl.get("Sprache", "Deutsch").strip() + + # Mechanismen + mech = [] + for i in range(1, 5): + m = xl.get(f"Mechanismus {i}", "").strip() + if m: mech.append(m) + + # Pro/Kontra + pros = [] + for i in range(1, 5): + p = xl.get(f"Pro {i}", "").strip() + if p: pros.append(p) + kontras = [] + for i in range(1, 5): + k = xl.get(f"Kontra {i}", "").strip() + if k: kontras.append(k) + + # Tags + tags = [] + for i in range(1, 11): + t = xl.get(f"Tag {i}", "").strip() + if t: tags.append(t) + + # Disclosure aus Verlag + verlag_name = meta.get("verlag", verlag).strip() + disclosure_id = meta.get("disclosure", "verlag_gestellt").strip() disclosure_text = "" for d in cfg.get("disclosures", []): if d["id"] == disclosure_id: disclosure_text = d["text"].replace("{VERLAG}", verlag_name or "der Verlag").replace("{SPIEL}", spielname) break - # LLM-Strukturierung - teaser = word_text[:300] - erklarung = word_text - fazit = "" - try: - prompt = (f'Analysiere den Brettspiel-Review-Text fur "{spielname}". ' - f'Extrahiere:\n1. TEASER (1-3 einleitende Satze)\n' - f'2. SPIELERKLARUNG (ausfuhrliche Spielbeschreibung)\n' - f'3. FAZIT (personliche Meinung/Bewertung)\n\n' - f'Ausgabeformat:\n---TEASER---\n[Text]\n---ERKLAERUNG---\n[Text]\n---FAZIT---\n[Text]\n\n' - f'Text:\n{word_text[:8000]}') - from urllib.request import Request, urlopen - api_body = json.dumps({ - "model": "deepseek-v4-flash:cloud", "provider": "datenhimmel", - "messages": [{"role": "system", "content": "Du bist ein praziser Textanalysator."}, - {"role": "user", "content": prompt}], - "temperature": 0.1 - }).encode() - req = Request("https://ui.datenhimmel.work/api/v1/chat/completions", - data=api_body, headers={"Content-Type": "application/json", - "Authorization": "Bearer sk-ce3...b6db"}) - resp = urlopen(req, timeout=30) - result = json.loads(resp.read().decode()) - llm_text = result["choices"][0]["message"]["content"] - tm = re.search(r'---TEASER---\s*(.*?)\s*---ERKLAERUNG---', llm_text, re.DOTALL) - em = re.search(r'---ERKLAERUNG---\s*(.*?)\s*---FAZIT---', llm_text, re.DOTALL) - fm = re.search(r'---FAZIT---\s*(.*)', llm_text, re.DOTALL) - if tm: teaser = tm.group(1).strip() - if em: erklarung = em.group(1).strip() - if fm: fazit = fm.group(1).strip() - except Exception as e: - print(f"[review-builder] LLM fehlgeschlagen: {e}", flush=True) - # Bilder speichern & resizen + + # Bilder speichern erster = spielname[0].lower() if spielname else "x" ordner = spielname.lower().replace(" ", "") base_img = f"images/Bilderverzeichnis/01/{erster}/{ordner}" @@ -2062,53 +2085,89 @@ document.getElementById('publishBtn').addEventListener('click',function(){{submi _resize_image(img_data, os.path.join(wd, f"bild_{idx+1}.jpg")) except Exception as e: print(f"[review-builder] Bild {idx} fehlgeschlagen: {e}", flush=True) + # HTML bauen datum = datetime.now().strftime("%d. %B %Y") autor_name = author_info["name"] autor_banner = author_info["banner"] - if not teaser.startswith("<"): teaser = f"

{teaser}

" - if not erklarung.startswith("<"): erklarung = f"

{erklarung.replace(chr(10)+chr(10), '

')}

" - if fazit and not fazit.startswith("<"): fazit = f"

{fazit.replace(chr(10)+chr(10), '

')}

" + + # Infobox-Tabelle + fakten_rows = [] + if autoren: fakten_rows.append(f'Autor:innen{autoren}') + if illustratoren: fakten_rows.append(f'Illustrator:innen{illustratoren}') + if verlag: fakten_rows.append(f'Verlag{verlag}') + if personen: fakten_rows.append(f'Personenzahl{personen}') + if spielzeit: fakten_rows.append(f'Spielzeit{spielzeit}') + if jahr: fakten_rows.append(f'Erscheinungsjahr{jahr}') + if preis: fakten_rows.append(f'Preis{preis}') + if app: fakten_rows.append(f'App{app}') + if alter: fakten_rows.append(f'Alter{alter}') + fakten_html = "" + if fakten_rows: + fakten_html = '' + "".join(fakten_rows) + '
' + + # Pro/Kontra + pro_html = "" + if pros: + pro_html = '

👍 PRO

    ' + "".join(f"
  • {p}
  • " for p in pros) + '
' + kontra_html = "" + if kontras: + kontra_html = '

👎 KONTRA

    ' + "".join(f"
  • {k}
  • " for k in kontras) + '
' + + # Mechanismen + mech_html = "" + if mech: + mech_html = '

Mechanismen: ' + ", ".join(mech) + '

' + + # Bewertung + wertung_html = "" + if wertung: + wertung_html = f'
{wertung}/ 100
' + disc_html = "" if disclosure_text: disc_html = f'

{disclosure_text}
Dies hat keinen Einfluss auf unsere Bewertung!

' + aff_html = "" if fantasywelt_url: - aff_html += f'

Jetzt bei Fantasywelt kaufen

\n' + aff_html += f'

Bei Fantasywelt kaufen

\n' if so_url: aff_html += f'

Bei Spiele-Offensive kaufen

\n' + gal_path = f"01/{erster}/{ordner}" - body = f"""{teaser} + tags_str = ", ".join(tags) if tags else f"{spielname}, Brettspiel, Review, Bewertung, {autor_name}" + title_full = f"{spielname} {zusatz}".strip() + + body = f"""

{spielname} — {komplex or 'Brettspiel'} von {autoren or verlag or 'unbekannt'}


 

Fakten zum Spiel

+{fakten_html} {disc_html} -

So spielt sich {spielname}

-{erklarung} -
+{mech_html} +{pro_html} +{kontra_html}

Meine Meinung

-{fazit} +{wertung_html}

 

-

Wertung zum Spiel

-

 

-

{{module Discord-Hinweis Tests}}

+

{{{{module Discord-Hinweis Tests}}}}

 

{aff_html}

 

Bilder vom Spiel

-

{{gallery}}{gal_path}{{/gallery}}

-

{{module Aktuelle Texte fur Content}}

""" +

{{{{gallery}}}}{gal_path}{{{{/gallery}}}}

+

{{{{module Aktuelle Texte fur Content}}}}

""" + datei = datetime.now().strftime("%Y-%m-%d") safe = spielname.lower().replace(" ", "-").replace(":", "").replace("/", "-") filename = f"artikel_{safe}_{datei}.html" filepath = os.path.join(WORKSPACE, filename) - desc = teaser.replace("

", "").replace("

", "")[:150] full = f""" - - + + -{spielname} \u2014 Review von {autor_name} | Brettspiel-News.de +{title_full} — Review von {autor_name} | Brettspiel-News.de

{datum} | von {autor_name}

{body}
""" with open(filepath, "w", encoding="utf-8") as f: f.write(full)