Files
BSN-Chatsystem/build_article.py
Hermes Agent d02c9906a9 feat: Review-Builder — Word-Datei + Bilder hochladen, LLM-strukturiert HTML bauen
- GET /review-builder: Upload-Formular (Word .docx + Bilder Drag/Drop + Metadaten)
- POST /review-builder/build: Word parsen → LLM (DeepSeek Flash) strukturiert
  Teaser/Spielerklärung/Fazit → HTML bauen → im Workspace speichern
- Bild-Resize: 1920x1080 @ 72dpi (PIL), Bilderverzeichnis/01/{buchstabe}/{spiel}/
- review_config.json: 25+ Autoren, 5 Disclosures, 5 Rubriken, Komplexitätsstufen
- Live-Publish via Joomla-API optional (Button: Vorschau / Veröffentlichen)
- Link im Dashboard: 📝 Review-Builder
2026-07-13 16:36:00 +02:00

229 lines
8.9 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""Generate the crowdfunding article HTML from merged data."""
import json, os, sys
from datetime import datetime, timezone
now = datetime.now(timezone.utc)
kw = now.isocalendar()[1]
today_de = now.strftime("%d. %B %Y")
# German month names
months_de = {"January":"Januar","February":"Februar","March":"März","April":"April","May":"Mai","June":"Juni",
"July":"Juli","August":"August","September":"September","October":"Oktober","November":"November","December":"Dezember"}
for en, de in months_de.items():
today_de = today_de.replace(en, de)
with open("/home/hermes/workspace/merged_data.json") as f:
data = json.load(f)
# Sort by USD funding
data.sort(key=lambda x: x["funding_usd"], reverse=True)
top10 = data[:10]
endspurte = [p for p in data if p["days_left"] <= 3 and p["funding_usd"] >= 500]
# Geheimtipps: well-funded smaller campaigns
geheimtipps = [p for p in data if p["funding_usd"] >= 1000 and p["pct"] >= 100
and p["backers"] >= 30 and p["funding_usd"] < 50000
and p not in top10]
def fmt_funds(p):
if p["currency"] == "EUR":
return f'{p["funds"]:,.0f}'.replace(',', '.')
else:
return f'{p["funds"]:,.0f} $'.replace(',', '.')
def fmt_goal(p):
if p["currency"] == "EUR":
return f'{p["goal"]:,.0f}'.replace(',', '.')
else:
return f'{p["goal"]:,.0f} $'.replace(',', '.')
def fmt_pct(p):
return f'{p["pct"]} %' if p["pct"] == int(p["pct"]) else f'{p["pct"]:.0f} %'
def days_class(d):
if d <= 1: return "days-urgent"
elif d <= 3: return "days-warning"
return "days-ok"
def rank_class(n):
if n == 1: return "rank-1"
elif n == 2: return "rank-2"
elif n == 3: return "rank-3"
return ""
def platform_cell(p):
if p["platform"] == "Gamefound":
return '<td style="text-align:center"><span class="badge"><img src="https://www.brettspiel-news.de/images/Spiele/2026/logo_gamefound.png" alt="Gamefound" /></span></td>'
else:
return '<td style="text-align:center"><span class="badge"><img src="https://www.brettspiel-news.de/images/Spiele/2026/logo_kickstarter.png" alt="Kickstarter" /></span></td>'
def cover_img(p):
if p["image"]:
# Use small thumbnail - Gamefound images might be too large, use inline CSS resize
return f'<img src="{p["image"]}" alt="{p["name"]}" style="width:60px;height:60px;object-fit:cover;border-radius:4px;" loading="lazy" />'
return ""
def game_cell(p, rank=None):
img_html = cover_img(p)
url = p["url"]
name = p['name']
if rank and rank <= 3:
name_html = f'<a href="{url}" target="_blank"><strong>{name}</strong></a>'
elif rank and rank <= 10:
name_html = f'<a href="{url}" target="_blank"><strong>{name}</strong></a>'
else:
name_html = f'<a href="{url}" target="_blank">{name}</a>'
if img_html:
return f'<td><div style="display:flex;align-items:center;gap:0.5rem;">{img_html}<div>{name_html}</div></div></td>'
else:
return f'<td>{name_html}</td>'
def table_row(p, rank=None, extra_class=""):
r = rank or 0
rc = rank_class(r) if r <= 3 else ""
badge = f'<span class="rank-badge {rc}">{r}</span>' if r else ""
cls = extra_class
dc = days_class(p["days_left"])
# Determine row class
if p["funding_usd"] >= 100000:
if "row-mega" not in cls:
cls = (cls + " row-mega").strip()
row = '<tr'
if cls:
row += f' class="{cls}"'
row += '>\n'
if rank:
row += f' <td class="rank">{badge}</td>\n'
row += f' {game_cell(p, rank=r)}\n'
row += f' {platform_cell(p)}\n'
row += f' <td class="funding">{fmt_funds(p)}</td>\n'
row += f' <td class="pct">{fmt_pct(p)}</td>\n'
row += f' <td class="num">{p["backers"]:,}</td>\n'.replace(',', '.')
row += f' <td class="num"><span class="{dc}">{p["days_left"]}</span></td>\n'
row += '</tr>'
return row
# === BUILD HTML ===
html = f"""<!DOCTYPE html>
<html lang="de">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Crowdfunding-Übersicht KW {kw}: {len(data)} Brettspiel-Kampagnen auf Kickstarter und Gamefound im Check — Top 10, Endspurte und Geheimtipps.">
<meta name="keywords" content="Crowdfunding, Brettspiele, Kickstarter, Gamefound, KW {kw}">
<meta name="bsn-tags" content="Crowdfunding, Strategie, Kartenspiel">
<meta name="bsn-cover" content="images/Spiele/2026/crowdfunding_cover_1920x1080_2026_093650.jpg">
<title>Crowdfunding-Übersicht KW {kw} {len(data)} Brettspiel-Kampagnen im Check</title>
</head>
<body>
<p class="meta">{today_de}</p>
<!-- bsn-meta:{today_de} -->
<p class="dachzeile">KW {kw}: {len(top10)} dicke Fische und {len(endspurte)} Endspurte — wir checken alle {len(data)} aktiven Brettspiel-Kampagnen auf Kickstarter und Gamefound.</p>
<article>
<p>Die KW {kw} bringt satte {len(data)} aktive Brettspiel-Kampagnen auf Kickstarter und Gamefound. Darunter echte Schwergewichte wie <strong>{top10[0]['name']}</strong> mit über {fmt_funds(top10[0])} — aber auch {sum(1 for p in data if p['days_left'] <= 1)} Kampagnen, die heute oder morgen auslaufen. Wir haben alle für euch im Blick.</p>
<hr id="system-readmore" />
"""
# === TOP 10 TABLE ===
html += '<h2>Die Großen: Top 10 nach Funding</h2>\n'
html += '<table class="bsn-table">\n'
html += '<tr><th>#</th><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
for i, p in enumerate(top10):
extra = ""
# Mark mega campaigns
if p["funding_usd"] >= 250000:
extra = "row-mega"
elif p["funding_usd"] >= 100000:
extra = "row-highlight"
html += table_row(p, rank=i+1, extra_class=extra) + '\n'
html += '</table>\n\n'
# === ENDSPURTE ===
if endspurte:
es_sorted = sorted(endspurte, key=lambda x: x["days_left"])
html += '<h2>Endspurte: Diese Kampagnen laufen bald aus</h2>\n'
html += '<table class="bsn-table">\n'
html += '<tr><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
for p in es_sorted:
extra = "row-highlight" if p["pct"] >= 100 else ""
html += table_row(p, extra_class=extra) + '\n'
html += '</table>\n\n'
# === GEHEIMTIPPS ===
if geheimtipps:
html += '<h2>Kleine Verlage, große Kampagnen</h2>\n'
html += '<p>Diese kleineren Projekte haben ihr Ziel bereits erreicht und verdienen einen zweiten Blick:</p>\n'
html += '<table class="bsn-table">\n'
html += '<tr><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
for p in sorted(geheimtipps, key=lambda x: x["pct"], reverse=True)[:8]:
html += table_row(p, extra_class="row-highlight") + '\n'
html += '</table>\n\n'
# === ALL CAMPAIGNS ===
html += f'<h2>Alle {len(data)} Kampagnen im Überblick</h2>\n'
html += '<table class="bsn-table">\n'
html += '<tr><th>#</th><th>Spiel</th><th style="text-align:center">Plattform</th><th class="funding">Funding</th><th class="num">%</th><th class="num">Backer</th><th class="num">Tage</th></tr>\n'
for i, p in enumerate(data):
extra = ""
if i < 10:
extra = "row-highlight"
html += table_row(p, rank=i+1, extra_class=extra) + '\n'
html += '</table>\n\n'
# === SOURCES ===
html += f"""<span style="font-size:1.17em;font-weight:bold;display:block;margin:1em 0;"><strong>Quellen</strong></span>
<ol>
<li><a href="https://gamefound.com" target="_blank">Gamefound</a> — Aktive Crowdfunding-Projekte (Stand: {today_de})</li>
<li><a href="https://www.kickstarter.com" target="_blank">Kickstarter</a> — Tabletop Games Kategorie (Stand: {today_de})</li>
</ol>
<p class="bsn-recherche"><em>Recherchestand: {today_de}</em></p>
</article>
<!-- NANO-BANANA-PROMPT
Erstelle ein Titelbild für brettspiel-news.de — Crowdfunding-Übersicht KW {kw}:
- 16:9, 1920x1080, 72 DPI
- Stil: Editorial/Magazin, warme Beleuchtung
- Collage aus Brettspiel-Covern auf einem Holzspieltisch
- Im Zentrum: Logo-Integration "Crowdfunding KW {kw}"
- Kickstarter- und Gamefound-Logos dezent am Rand
- Farbpalette: warme Holztöne, BSN-Blau (#20228A) als Akzent
- Keine störenden Elemente im Hintergrund
-->
</body>
</html>"""
# Write output
out_path = "/home/hermes/workspace/crowdfunding_kw28_article.html"
with open(out_path, "w") as f:
f.write(html)
# Stats
print(f"Article written: {out_path}")
print(f"Projects: {len(data)} (GF: {sum(1 for p in data if p['platform']=='Gamefound')}, KS: {sum(1 for p in data if p['platform']=='Kickstarter')})")
print(f"Top 10 range: {fmt_funds(top10[-1])}{fmt_funds(top10[0])}")
print(f"Endspurte: {len(endspurte)}")
print(f"Geheimtipps: {len(geheimtipps)}")
print("DONE")