fix(nanobanana): Aspect-Ratio-Bug doppelt behoben

- Bug #1: img.resize((1920,1080)) war hartkodiert → auf _aspect_to_size() umgestellt
- Bug #2: imageConfig.aspectRatio fehlte im Gemini-API-Call → ergänzt

Alle Aspect-Ratios (1:1, 2:3, 9:16, 21:9 etc.) werden jetzt korrekt gerendert.
This commit is contained in:
Hermes Agent
2026-06-28 22:51:36 +02:00
parent 0a63f0439f
commit ff334be4c4
+129 -4
View File
@@ -30,6 +30,90 @@ ACCESS_TOKEN = "br3ttsp1el-n3ws-2026"
USERNAME = "DK-Adminchef" USERNAME = "DK-Adminchef"
PASSWORD = "Mcik7%vbdhXa_" PASSWORD = "Mcik7%vbdhXa_"
# ── BSN Article CSS (aus Joomla bsn-custom.css) ───────────────────────────
# Wird in Artikel-HTML injiziert, damit bsn-Klassen auch auf dem
# Article-Server gerendert werden (nicht nur im Joomla-Template).
BSN_ARTICLE_CSS = """
/* ── Basis-Typografie (Article-Server) ── */
body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Helvetica, Arial, sans-serif; font-size: 16px; line-height: 1.7; color: #3B3B3C; background: #fff; max-width: 720px; margin: 0 auto; padding: 2rem 1.5rem; -webkit-font-smoothing: antialiased; }
article { display: block; }
article h1, article h1.title { font-size: 2rem; font-weight: 800; color: #171D4F; margin: 0 0 0.5rem 0; line-height: 1.2; letter-spacing: -0.02em; }
article h2 { font-size: 1.4rem; font-weight: 700; color: #171D4F; margin: 2rem 0 0.75rem; }
article h3 { font-size: 1.15rem; font-weight: 700; color: #20228A; margin: 1.5rem 0 0.5rem; }
article p { margin: 0 0 1rem 0; }
article a { color: #20228A; text-decoration: underline; text-underline-offset: 2px; }
article a:hover { color: #161e6e; }
article ul, article ol { margin: 0.5rem 0 1rem 1.5rem; }
article li { margin-bottom: 0.4rem; }
article img { max-width: 100%; height: auto; border-radius: 8px; margin: 1.5rem 0; }
article hr { border: none; border-top: 1px solid #e0e0e0; margin: 2rem 0; }
/* ── Meta / Dachzeile ── */
.bsn-meta, .meta, p.meta { font-size: 0.85rem; color: #999; margin-bottom: 1.5rem; }
.bsn-dachzeile, .dachzeile, p.dachzeile { font-size: 1.05rem; color: #555; margin-bottom: 1.5rem; font-weight: 500; }
/* ── Infobox ── */
.bsn-infobox { background: #f2f2f2; border: 1px solid #d0d8e0; border-left: 4px solid #20228A; padding: 1rem 1.5rem; margin: 1.5rem 0; border-radius: 16px; font-size: 0.95rem; }
.bsn-infobox h3 { margin-top: 0; color: #20228A; font-family: Arial, Helvetica, sans-serif; font-size: 1.15rem; }
.bsn-infobox table { width: 100%; border-collapse: collapse; }
.bsn-infobox td { padding: 0.3rem 0.5rem; border-bottom: 1px solid #dce4ec; }
.bsn-infobox tr:last-child td { border-bottom: none; }
.bsn-infobox td:first-child { font-weight: 600; color: #171D4F; width: 160px; white-space: nowrap; }
/* ── Tabellen ── */
.bsn-table { width: 100%; border-collapse: collapse; margin: 1rem 0; }
.bsn-table td { padding: 0.5rem 0.8rem; border-bottom: 1px solid #e8e8f0; }
.bsn-table tr:last-child td { border-bottom: none; }
.bsn-table td:first-child { font-weight: 600; color: #171D4F; width: 30%; }
/* ── Gamecard ── */
.bsn-gamecard { background: #f2f2f2; border: 1px solid #e0e3e8; border-left: 4px solid #20228A; padding: 1rem 1.5rem; margin: 1.2rem 0; border-radius: 16px; transition: transform 0.2s ease, box-shadow 0.2s ease; }
.bsn-gamecard h3 { margin-top: 0; color: #171D4F; font-family: Arial, Helvetica, sans-serif; font-size: 1.15rem; }
.bsn-gamecard .game-meta { font-size: 0.85rem; color: #888; margin-bottom: 0.5rem; }
@media (hover: hover) { .bsn-gamecard:hover { transform: translateY(-3px); box-shadow: 0 6px 20px rgba(32,34,138,0.12); border-color: #c8cde0; } }
/* ── Quellen / Recherche ── */
.bsn-quellen { margin-top: 2rem; font-size: 0.9rem; }
.bsn-quellen h3 { color: #171D4F; font-family: Arial, Helvetica, sans-serif; font-size: 1.1rem; margin-bottom: 0.5rem; }
.bsn-quellen ul { padding-left: 1.3rem; margin: 0; }
.bsn-quellen li { margin-bottom: 0.3rem; }
.bsn-recherche, .recherche, p.bsn-recherche { margin-top: 2.5rem; padding-top: 1rem; border-top: 1px solid #ddd; font-size: 0.9rem; color: #777; }
/* ── Blockquote ── */
blockquote { border-left: 4px solid #20228A; background: #f8f9ff; padding: 0.8rem 1.2rem; margin: 1.2rem 0; color: #3B3B3C; font-style: italic; border-radius: 0 8px 8px 0; }
blockquote p { margin-bottom: 0.25rem; }
/* ── System-Readmore (Joomla) ── */
hr#system-readmore { border: none; border-top: 2px dashed #e0e0e0; margin: 1.5rem 0; }
/* ── Quellen (alte Klasse) ── */
.quellen { margin-top: 2rem; font-size: 0.9rem; }
.quellen h3, .quellen strong { color: #171D4F; font-size: 1.1rem; display: block; margin-bottom: 0.5rem; }
.quellen ol, .quellen ul { padding-left: 1.3rem; margin: 0.5rem 0; }
.quellen li { margin-bottom: 0.3rem; }
@media (prefers-color-scheme: dark) {
body { background: #1a1a2e; color: #c8c8d8; }
article h1, article h2 { color: #e0e0f0; }
article h3 { color: #6b6bcc; }
.bsn-infobox, .bsn-gamecard { background: #252530; border-color: #3a3a45; }
.bsn-infobox td, .bsn-table td { border-bottom-color: #3a3a45; }
.bsn-infobox td:first-child, .bsn-table td:first-child { color: #a0a0c0; }
.bsn-infobox h3 { color: #6b6bcc; }
.bsn-gamecard h3 { color: #c8c8d8; }
.bsn-gamecard .game-meta { color: #888; }
.bsn-quellen h3, .quellen h3, .quellen strong { color: #c8c8d8; }
.bsn-quellen a, .quellen a { color: #6b6bcc; }
.bsn-meta, .meta, p.meta { color: #777; }
.bsn-recherche, .recherche, p.bsn-recherche { color: #888; border-top-color: #3a3a45; }
.bsn-dachzeile, .dachzeile, p.dachzeile { color: #999; border-bottom-color: #3a3a45; }
.bsn-gamecard:hover { box-shadow: 0 6px 20px rgba(100,100,200,0.15); border-color: #505068; }
blockquote { background: #252530; border-left-color: #6b6bcc; color: #c0c0d0; }
hr#system-readmore { border-top-color: #3a3a45; }
hr { border-top-color: #3a3a45; }
}
"""
# ── Joomla API Configuration ────────────────────────────────────────────── # ── Joomla API Configuration ──────────────────────────────────────────────
JOOMLA_API_URL = "https://www.brettspiel-news.de/api/index.php/v1/content/articles" JOOMLA_API_URL = "https://www.brettspiel-news.de/api/index.php/v1/content/articles"
# JOOMLA_TOKEN is read from environment or from a token file # JOOMLA_TOKEN is read from environment or from a token file
@@ -140,7 +224,10 @@ def _gemini_generate_image(
body = json.dumps({ body = json.dumps({
"contents": [{"parts": parts}], "contents": [{"parts": parts}],
"generationConfig": { "generationConfig": {
"responseModalities": ["image", "text"] "responseModalities": ["image", "text"],
"imageConfig": {
"aspectRatio": aspect_ratio
}
} }
}).encode() }).encode()
@@ -188,7 +275,7 @@ SKIP_PATTERNS = [
r'^envyborn', r'^brettspiel_news_newsletter', r'^boardgamewire', r'^envyborn', r'^brettspiel_news_newsletter', r'^boardgamewire',
r'^ddg_', r'^blocked', r'^error', r'^just.a.moment', r'^ddg_', r'^blocked', r'^error', r'^just.a.moment',
r'^duckduckgo', r'^update.regarding', r'^duckduckgo', r'^update.regarding',
r'deals', r'kennerspiele', r'kombi_deals', r'top_deals', r'kennerspiele_deals', r'kombi_deals', r'top_deals',
r'kinderspiele_deals', r'amazon_deals', r'amazon_primeday', r'kinderspiele_deals', r'amazon_deals', r'amazon_primeday',
] ]
@@ -943,6 +1030,17 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
if os.path.isfile(filepath) and filepath.startswith(WORKSPACE): if os.path.isfile(filepath) and filepath.startswith(WORKSPACE):
with open(filepath, "rb") as f: with open(filepath, "rb") as f:
data = f.read() data = f.read()
if filepath.endswith(".html"):
# BSN-CSS in Artikel injizieren (vor </head>)
html_str = data.decode("utf-8", errors="replace")
css_block = f"<style>\n{BSN_ARTICLE_CSS}\n</style>"
if "</head>" in html_str:
html_str = html_str.replace("</head>", f"{css_block}\n</head>", 1)
elif "<body" in html_str:
html_str = html_str.replace("<body", f"{css_block}\n<body", 1)
else:
html_str = f"<!DOCTYPE html>\n<html><head>{css_block}</head><body>{html_str}</body></html>"
data = html_str.encode("utf-8")
self.send_response(200) self.send_response(200)
self.send_header("Content-Length", str(len(data))) self.send_header("Content-Length", str(len(data)))
if filepath.endswith(".html"): if filepath.endswith(".html"):
@@ -1085,12 +1183,39 @@ h1{color:#c0392b;}p{color:#888;}</style></head>
with open(filepath, "wb") as f: with open(filepath, "wb") as f:
f.write(img_bytes) f.write(img_bytes)
# Optional: PIL-Konvertierung zu JPEG (1920x1080 @ 72 DPI) # Optional: PIL-Konvertierung zu JPEG (aspect-ratio-bewusst)
try: try:
from PIL import Image from PIL import Image
# Zielauflösung aus Aspect-Ratio berechnen
def _aspect_to_size(ratio_str: str, base: int = 1920) -> tuple[int, int]:
"""Aspect-Ratio-String in Ziel-Pixelmaße umrechnen."""
mapping = {
"16:9": (1920, 1080),
"1:1": (1080, 1080),
"2:3": (720, 1080),
"3:2": (1620, 1080),
"3:4": (810, 1080),
"4:3": (1440, 1080),
"4:5": (864, 1080),
"5:4": (1350, 1080),
"9:16": (607, 1080),
"21:9": (1920, 823),
}
# Fallback: selbst parsen
try:
w, h = ratio_str.split(":")
w_ratio, h_ratio = int(w), int(h)
# Immer Höhe = 1080, Breite proportional
width = int(1080 * w_ratio / h_ratio)
return mapping.get(ratio_str, (width, 1080))
except (ValueError, ZeroDivisionError):
return (1920, 1080)
target_w, target_h = _aspect_to_size(aspect_ratio)
img = Image.open(filepath) img = Image.open(filepath)
img = img.convert("RGB") img = img.convert("RGB")
img = img.resize((1920, 1080), Image.LANCZOS) img = img.resize((target_w, target_h), Image.LANCZOS)
jpg_path = os.path.join(month_dir, base_name + ".jpg") jpg_path = os.path.join(month_dir, base_name + ".jpg")
img.save(jpg_path, "JPEG", dpi=(72, 72), quality=92) img.save(jpg_path, "JPEG", dpi=(72, 72), quality=92)
filepath = jpg_path filepath = jpg_path