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>
216 lines
10 KiB
Python
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()
|