Files
HiveGrid-Marketing/webinar/slides/bauen.py
T
dejanandClaude Opus 5.5 686ee36b4f HiveGrid Marketing: Hub mit Strategie, Redaktionsplan, Dashboard, Produktionsvorgaben und Webinar
Stand 16.10.2026. Enthält das Marketing-Dashboard, die Daten in daten/,
die Produktionsvorgaben, Reels und das Webinar mit Skript, Sprechzettel
und Präsentation (HTML, PowerPoint, PDF).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-16 18:09:07 +02:00

216 lines
10 KiB
Python

#!/usr/bin/env python3
"""Baut aus quelle/ eine Datei Praesentation.html, die man lokal im Browser öffnet.
Die echte Präsentation liegt als Artifact auf claude.ai (Link in README.md).
quelle/ ist eine Kopie ihrer Dateien. Die Bilder heißen dort /_blob/<id>,
hier werden sie auf bilder/ umgebogen. Liegt ein Screenshot unter
screenshots/S01.png usw., ersetzt er in der lokalen Ansicht den Platzhalter.
Aufruf: python3 bauen.py
"""
import json
import re
from pathlib import Path
HIER = Path(__file__).resolve().parent
QUELLE = HIER / "quelle"
# Asset-IDs im Artifact und ihre Datei hier
BLOBS = {
"fbb76cc6172b1dda37e6b3905505d88d": "bilder/logo.png",
"891500b0a252531996892fe409bd07b6": "bilder/noah-karte.png",
"eabbac8a2c57c8d3a9ebc7c70a2a1057": "bilder/kwp.png",
"52006618bf131aa5724ef876e8cc1a2e": "bilder/pds.png",
"da2cab0c5d17272aca652d9424d6970d": "bilder/whatsapp.png",
"044436d32cb349e58d3f5149e6a8c470": "bilder/excel.png",
"95b45199d28584cd97be14c33a7bbe36": "bilder/wabe-ecke.png",
# Screenshots, im Artifact als Asset hochgeladen
"566587d5258a7d5718251f38482d0752": "screenshots/S01.png",
"fbbba402d8ac3fa4dc35c26731ede795": "screenshots/S02.png",
"248a5326ebfdec0f3955defb21c24634": "screenshots/S03.png",
"d38901c42a7861c9ef7056d701b0c100": "screenshots/S04.png",
"558cb83a2fc7a5036654f37e80547a9f": "screenshots/S05.png",
"e75b4f40d345222cdf2be8a0e01a8f14": "screenshots/S06.png",
"6b42b078e66f33b4880a9f6b64e1282e": "screenshots/S07.png",
"8da95c03c2d3d0e5828d9ce790986e71": "screenshots/S08.png",
"20d28a68c97fe007e28785b6b3867f8e": "screenshots/S09.png",
"46654795dbfc8ef3e14676fed2623587": "screenshots/S10.png",
"d87fb3ebfe2b939903850b2d3bc9b9bc": "screenshots/S11.png",
"6921af16f301ba8795f1e9e83c5e3686": "screenshots/S12.png",
"0e3cf8f223f0f778f19fe59fabf9bab5": "screenshots/S13.png",
"6447f63b5cf121c22c778f9e54b24362": "screenshots/S14.png",
"017ed2d7b3500b4713bc0dbccb24c128": "screenshots/S16.png",
"98224937c2134633f62e03ca48798dd8": "screenshots/S17.png",
}
# Ein Platzhalter ist der graue Kasten mit "Screenshot Sxx" darin.
PLATZHALTER = re.compile(
r'<div style="(width:(\d+)px;height:(\d+)px;)background:#EFEFF0;[^"]*">\s*'
r'<p style="[^"]*">Screenshot (S\d\d)</p>.*?</div>',
re.S,
)
def screenshot_einsetzen(html):
def ersetzen(m):
breite, hoehe, nummer = m.group(2), m.group(3), m.group(4)
# "border:none" im Platzhalter: Bild ohne eigenen Rahmen, zum Beispiel im Handy-Rahmen
ohne_rahmen = "border:none" in m.group(1) + m.group(0)[:300]
for endung in ("png", "jpg", "jpeg", "webp"):
datei = HIER / "screenshots" / f"{nummer}.{endung}"
if datei.exists():
if ohne_rahmen:
return (f'<img src="screenshots/{datei.name}" alt="{nummer}" '
f'style="width:{breite}px;height:{hoehe}px;object-fit:cover;object-position:top;display:block">')
return (f'<img src="screenshots/{datei.name}" alt="{nummer}" '
f'style="width:{breite}px;height:{hoehe}px;object-fit:contain;'
f'border:1px solid #CECECF;border-radius:12px;background:#FFFFFF">')
return m.group(0)
return PLATZHALTER.sub(ersetzen, html)
def main():
deck = json.loads((QUELLE / "deck.json").read_text(encoding="utf-8"))
fonts = "".join(
f'<link rel="stylesheet" href="{f["href"]}">'
for f in deck.get("faces", {}).values() if "href" in f
)
hoch = json.loads((HIER / "uebergaenge.json").read_text(encoding="utf-8")).get("hoch", [])
folien = []
for nr, sid in enumerate(deck["order"], 1):
html = (QUELLE / "slides" / f"{sid}.html").read_text(encoding="utf-8")
for blob, pfad in BLOBS.items():
html = html.replace(f"/_blob/{blob}", pfad)
html = screenshot_einsetzen(html)
notiz = re.search(r"<aside>(.*?)</aside>", html, re.S)
notiz = notiz.group(1).strip() if notiz else ""
folien.append(
f'<div class="folie" id="f{nr}" data-sid="{sid}"><div class="rahmen">{html}</div>'
f'<p class="notiz"><b>{nr}, {sid}</b>\n{notiz}</p></div>'
)
seite = f"""<!doctype html>
<html lang="de"><head><meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>{deck["title"]}</title>{fonts}
<style>
body {{ margin: 0; background: #3A3A40; font-family: 'Mona Sans', Arial, sans-serif; }}
.kopf {{ color: #E5E5E5; padding: 16px 24px; font-size: 14px; }}
.folie {{ max-width: 1440px; margin: 0 auto 32px; padding: 0 16px; }}
.rahmen {{ position: relative; width: 100%; aspect-ratio: 16 / 9; overflow: hidden; background: #FAFAFA; }}
.rahmen > section {{ position: absolute; left: 0; top: 0; width: 1920px; height: 1080px;
transform-origin: 0 0; box-sizing: border-box; overflow: hidden; }}
section * {{ margin: 0; box-sizing: border-box; }}
section h1 {{ font-size: 96px; font-weight: 600; line-height: 1.1; }}
section h2 {{ font-size: 64px; font-weight: 600; line-height: 1.15; }}
section h3 {{ font-size: 44px; font-weight: 600; line-height: 1.2; }}
section p {{ font-size: 32px; line-height: 1.4; }}
section img, section svg {{ display: block; flex: none; }}
section aside {{ display: none; }}
.notiz {{ color: #C8C8CC; font-size: 14px; line-height: 1.5; margin: 8px 0 0; white-space: pre-line; }}
body.zeigen {{ background: #000; overflow: hidden; }}
body.zeigen .kopf, body.zeigen .notiz {{ display: none; }}
body.zeigen .folie {{ display: none; max-width: none; margin: 0; padding: 0; }}
body.zeigen .folie.aktiv {{ display: flex; align-items: center; height: 100vh; }}
body.zeigen .rahmen {{ width: min(100vw, 177.78vh); margin: 0 auto; }}
body.zeigen .folie.wechsel {{ position: fixed; inset: 0; display: flex; align-items: center; }}
</style></head><body>
<p class="kopf">{deck["title"]}, {len(folien)} Slides. Lokale Ansicht, gebaut aus quelle/.
Taste P: Präsentieren, Pfeiltasten: weiter und zurück, Esc: zurück zur Übersicht.</p>
{"".join(folien)}
<script>
const folien = [...document.querySelectorAll('.folie')];
let aktiv = 0;
function skalieren() {{
document.querySelectorAll('.rahmen').forEach(r => {{
r.firstElementChild.style.transform = 'scale(' + (r.clientWidth / 1920) + ')';
}});
}}
// Nach diesen Slides schiebt sich die nächste von unten hoch (uebergaenge.json),
// sonst wird sie eingeblendet wie im Online-Deck.
const HOCH = {json.dumps(hoch)};
let laeuft = null;
function zeige(i) {{
const alt = aktiv;
const neu = Math.max(0, Math.min(folien.length - 1, i));
const animiert = document.body.classList.contains('zeigen') && neu !== alt && Math.abs(neu - alt) === 1;
if (laeuft) return;
aktiv = neu;
if (!animiert) {{
folien.forEach((f, j) => {{ f.classList.toggle('aktiv', j === aktiv); f.classList.remove('wechsel'); }});
skalieren();
return;
}}
const a = folien[alt], n = folien[neu];
a.classList.add('aktiv', 'wechsel'); n.classList.add('aktiv', 'wechsel');
skalieren();
const vor = neu > alt ? 1 : -1;
const hoch = HOCH.includes((vor > 0 ? a : n).dataset.sid);
const ra = a.querySelector('.rahmen'), rn = n.querySelector('.rahmen');
if (hoch) {{
// genaue Höhe minus ein Pixel Überlappung, sonst blitzt an der Naht der schwarze Grund durch
const h = rn.getBoundingClientRect().height - 1;
const art = {{ duration: 800, easing: 'cubic-bezier(.65,0,.35,1)' }};
laeuft = [
ra.animate([{{ transform: 'translateY(0)' }}, {{ transform: `translateY(${{-vor * h}}px)` }}], art),
rn.animate([{{ transform: `translateY(${{vor * h}}px)` }}, {{ transform: 'translateY(0)' }}], art),
];
}} else {{
n.style.zIndex = 1;
laeuft = [n.animate([{{ opacity: 0 }}, {{ opacity: 1 }}], {{ duration: 350, easing: 'ease-out' }})];
}}
laeuft[0].onfinish = () => {{
a.classList.remove('aktiv', 'wechsel'); n.classList.remove('wechsel'); n.style.zIndex = '';
laeuft = null;
}};
}}
addEventListener('resize', skalieren);
addEventListener('keydown', e => {{
if (e.key === 'p' || e.key === 'P') {{ document.body.classList.add('zeigen'); zeige(aktiv); }}
else if (e.key === 'Escape') {{ document.body.classList.remove('zeigen'); skalieren(); }}
else if (document.body.classList.contains('zeigen')) {{
if (['ArrowRight', 'ArrowDown', ' ', 'PageDown'].includes(e.key)) zeige(aktiv + 1);
if (['ArrowLeft', 'ArrowUp', 'PageUp'].includes(e.key)) zeige(aktiv - 1);
}}
}});
document.fonts.ready.then(skalieren);
skalieren();
</script>
</body></html>
"""
(HIER / "Praesentation.html").write_text(seite, encoding="utf-8")
print(f"Praesentation.html: {len(folien)} Slides")
# Druckfassung für exportieren.mjs: jede Slide eine Seite in 1920 x 1080,
# ohne Skalierung. Daraus entstehen PDF und PowerPoint.
seiten = []
for sid in deck["order"]:
html = (QUELLE / "slides" / f"{sid}.html").read_text(encoding="utf-8")
for blob, pfad in BLOBS.items():
html = html.replace(f"/_blob/{blob}", pfad)
seiten.append(f'<div class="seite">{screenshot_einsetzen(html)}</div>')
druck = f"""<!doctype html>
<html lang="de"><head><meta charset="utf-8"><title>{deck["title"]}</title>{fonts}
<style>
@page {{ size: 1920px 1080px; margin: 0; }}
html, body {{ margin: 0; padding: 0; background: #FAFAFA; }}
.seite {{ position: relative; width: 1920px; height: 1080px; overflow: hidden; break-after: page; }}
.seite > section {{ position: absolute; left: 0; top: 0; width: 1920px; height: 1080px; box-sizing: border-box; overflow: hidden; }}
section * {{ margin: 0; box-sizing: border-box; }}
section h1 {{ font-size: 96px; font-weight: 600; line-height: 1.1; }}
section h2 {{ font-size: 64px; font-weight: 600; line-height: 1.15; }}
section h3 {{ font-size: 44px; font-weight: 600; line-height: 1.2; }}
section p {{ font-size: 32px; line-height: 1.4; }}
section img, section svg {{ display: block; flex: none; }}
section aside {{ display: none; }}
</style></head><body>
{"".join(seiten)}
</body></html>
"""
(HIER / ".druck.html").write_text(druck, encoding="utf-8")
if __name__ == "__main__":
main()