- Live-Vorschau: Im MacBook steht die Beispiel-Website der Bäckerei Sonnenschein selbst (Startbereich, Sortiment, Backstube, Torten, Öffnungszeiten, Fuß; Instrument Serif, CC0-Fotos von Wikimedia Commons, nur Backwaren). Beim Scrollen durch die Station läuft sie einmal durch (--kp-v, nur transform). Portal-Aufnahmen (Projekte, Als App) neu mit derselben Website im Vorschaukasten. - Chat: Portal-Chat als Live-Demo. Die Kamera zoomt aufs Gespräch, Foto anhängen, tippen, Sprachnachricht, Antworten von Kenso ploppen auf (spielerische Texte, Sie-Form). Das iPhone gleitet in dieser Station weg. Hinweis auf die E-Mail bei Team-Antworten (K17). - „Funktionen“: Unterpunkte falten sich beim Verlassen ein, danach gleitet „Als App“ per FLIP nach oben (auch beim Aufklappen nach unten), Pfeil zeigt den Zustand. - Schrittlängen je Station (normal, Demo, Vorschau, Chat), bei reduzierter Bewegung kurz. - Tests: Bühne angepasst, neue Tests für Chat-Ablauf und Bäckerei-Website (287 grün). Spezifikation: docs/superpowers/specs/2026-10-04-vorschau-chat.md Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
95 lines
4.1 KiB
Python
95 lines
4.1 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Relaunch: Fotos der erfundenen Beispiel-Website „Bäckerei Sonnenschein“ (Station Live-Vorschau und Chat-Demo).
|
|
|
|
Alle Fotos stammen von Wikimedia Commons und stehen unter CC0 (gemeinfrei, keine Namensnennung nötig).
|
|
Es sind nur Backwaren zu sehen, keine Menschen. Quellen: docs/relaunch/assets/baeckerei/QUELLEN.md.
|
|
|
|
Wikimedia Commons (einmalig, Zwischenspeicher docs/relaunch/assets/baeckerei/roh/, nicht im Repository)
|
|
-> public/relaunch/demos/baeckerei/<name>-<breite>.webp
|
|
|
|
Aufruf (aus dem Projektverzeichnis): python3 scripts/relaunch-baeckerei-fotos.py
|
|
Braucht Python 3 und Pillow mit WebP-Unterstützung.
|
|
"""
|
|
from pathlib import Path
|
|
import sys
|
|
import urllib.parse
|
|
import urllib.request
|
|
|
|
from PIL import Image
|
|
|
|
WURZEL = Path(__file__).resolve().parent.parent
|
|
ROH = WURZEL / "docs" / "relaunch" / "assets" / "baeckerei" / "roh"
|
|
ZIEL = WURZEL / "public" / "relaunch" / "demos" / "baeckerei"
|
|
KENNUNG = {"User-Agent": "WEBklar-relaunch/1.0 (support@webklar.com)"}
|
|
|
|
KB = 1024
|
|
QUALITAET = 78
|
|
MIN_QUALITAET = 50
|
|
|
|
# Name -> (Datei auf Commons, Ausschnitt als Anteile (links, oben, rechts, unten), Zielbreiten, Seitenverhältnis B/H, Budget je Datei)
|
|
FOTOS = {
|
|
# Startseite und Chat-Foto: dasselbe Roggenbrot (Marlene schickt es im Chat, danach steht es auf der Website)
|
|
"roggenbrot": ("Rye bread Roggenbengel 2025-02-14 13.jpg", (0.0, 0.0, 1.0, 1.0), (600,), 1.08, 58 * KB),
|
|
"roggenbrot-anschnitt": ("Cut rye bread Kartoffelkruste 2025-02-20 02.jpg", (0.12, 0.12, 0.92, 0.95), (400,), 0.8, 30 * KB),
|
|
"broetchen": ("Bread-rolls.jpg", (0.0, 0.0, 1.0, 1.0), (400,), 0.8, 30 * KB),
|
|
"croissants": ("Freshippo croissant bread.jpg", (0.05, 0.0, 0.95, 1.0), (400,), 0.8, 34 * KB),
|
|
"zimtschnecken": ("IKEA cinnamon bun.jpg", (0.05, 0.0, 0.95, 1.0), (400,), 0.8, 30 * KB),
|
|
"baguettes": ("Baguette-246424.jpg", (0.0, 0.0, 1.0, 1.0), (700,), 1.2, 40 * KB),
|
|
"zwetschgenkuchen": ("Plum cake, Ehrenbach.jpg", (0.18, 0.08, 1.0, 1.0), (520,), 1.0, 50 * KB),
|
|
}
|
|
|
|
|
|
def lade(datei: str) -> Path:
|
|
"""Lädt das Foto einmal in 2000 px Breite von Commons (Special:FilePath) und hält es im Zwischenspeicher."""
|
|
ROH.mkdir(parents=True, exist_ok=True)
|
|
ziel = ROH / datei.replace(" ", "_")
|
|
if not ziel.exists():
|
|
url = "https://commons.wikimedia.org/w/index.php?title=Special:FilePath/" + urllib.parse.quote(datei) + "&width=2000"
|
|
with urllib.request.urlopen(urllib.request.Request(url, headers=KENNUNG), timeout=60) as antwort:
|
|
ziel.write_bytes(antwort.read())
|
|
return ziel
|
|
|
|
|
|
def zuschneiden(bild: Image.Image, anteile: tuple, verhaeltnis: float) -> Image.Image:
|
|
"""Schneidet erst den Ausschnitt aus, dann mittig auf das Seitenverhältnis."""
|
|
b, h = bild.size
|
|
l, o, r, u = anteile
|
|
bild = bild.crop((round(l * b), round(o * h), round(r * b), round(u * h)))
|
|
b, h = bild.size
|
|
if b / h > verhaeltnis:
|
|
neu = round(h * verhaeltnis)
|
|
return bild.crop(((b - neu) // 2, 0, (b - neu) // 2 + neu, h))
|
|
neu = round(b / verhaeltnis)
|
|
return bild.crop((0, (h - neu) // 2, b, (h - neu) // 2 + neu))
|
|
|
|
|
|
def speichern(bild: Image.Image, ziel: Path, budget: int) -> tuple[int, int]:
|
|
qualitaet = QUALITAET
|
|
while True:
|
|
bild.save(ziel, "WEBP", quality=qualitaet, method=6)
|
|
groesse = ziel.stat().st_size
|
|
if groesse <= budget or qualitaet - 4 < MIN_QUALITAET:
|
|
return groesse, qualitaet
|
|
qualitaet -= 4
|
|
|
|
|
|
def main() -> int:
|
|
ZIEL.mkdir(parents=True, exist_ok=True)
|
|
for alt in ZIEL.glob("*.webp"):
|
|
alt.unlink()
|
|
summe = 0
|
|
for name, (datei, anteile, breiten, verhaeltnis, budget) in FOTOS.items():
|
|
roh = zuschneiden(Image.open(lade(datei)).convert("RGB"), anteile, verhaeltnis)
|
|
for breite in breiten:
|
|
klein = roh.resize((breite, round(breite / verhaeltnis)), Image.LANCZOS)
|
|
groesse, q = speichern(klein, ZIEL / f"{name}-{breite}.webp", budget)
|
|
summe += groesse
|
|
print(f"{name}-{breite}.webp {klein.size[0]}x{klein.size[1]} {groesse / KB:.1f} KB q{q}")
|
|
print(f"Summe: {summe / KB:.0f} KB")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|