youtube/remotion/werkzeuge/posen.py
Pascal Scholz 0b326e6e9c Folge 2 „Pino ist müde“ komplett in Remotion (ohne Ton, ohne Higgsfield)
Szenen aus der Figuren-Bibliothek: freigestellte Standbilder mit
Posenwechsel, freigestellte Clips (WebM mit Alpha in /shared), Orte als
vorberechnete Aquarell-SVGs, Untertitel statt Stimmen. Zeitplan aus den
Sprechzeilen, damit ElevenLabs-Aufnahmen später nur die Dauer setzen.

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

68 lines
3.6 KiB
Python
Raw 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.

# Bereitet die freigestellten Standbilder der Figuren-Bibliothek für Remotion auf.
#
# Eingabe: freigestellte PNGs in voller Leinwand (1376 × 768, wie aus der
# Bibliothek), z. B. aus frei.py. Alle Bilder einer Figur werden mit demselben
# Rahmen zugeschnitten, damit Posen beim Überblenden deckungsgleich bleiben.
#
# Ausgabe: public/folge02/figuren/<pose>.webp und src/folge02/posen.ts
# python3 werkzeuge/posen.py <ordner_freigestellt> [<ordner_clips_frei>]
import json, os, sys, glob
from PIL import Image
QUELLE = sys.argv[1]
CLIPS = sys.argv[2] if len(sys.argv) > 2 else None
HIER = os.path.dirname(os.path.abspath(__file__))
ZIEL = os.path.join(HIER, "..", "public", "folge02", "figuren")
TS = os.path.join(HIER, "..", "src", "folge02", "posen.ts")
os.makedirs(ZIEL, exist_ok=True)
# Referenzpose je Figur: deren Höhe gilt als „Figurhöhe“, deren Fußpunkt als Anker.
REF = {"pino": "pino_ref_sitzend", "noa": "noa_ref_stehend", "lulu": "lulu_A6_blick-rechts"}
# Clip-Leinwand (1284 × 716) ist die verkleinerte Bild-Leinwand (1376 × 768).
K_CLIP = 1376 / 1284
posen = {}
for figur in REF:
dateien = sorted(glob.glob(f"{QUELLE}/{figur}_*.png"))
bilder = {os.path.basename(d)[:-4]: Image.open(d) for d in dateien}
boxen = {n: b.getchannel("A").getbbox() for n, b in bilder.items()}
rahmen = [min(b[0] for b in boxen.values()) - 4, min(b[1] for b in boxen.values()) - 4,
max(b[2] for b in boxen.values()) + 4, max(b[3] for b in boxen.values()) + 4]
rahmen = [max(0, rahmen[0]), max(0, rahmen[1]), min(1376, rahmen[2]), min(768, rahmen[3])]
for n, b in bilder.items():
b.crop(rahmen).save(f"{ZIEL}/{n}.webp", quality=90, method=5)
posen[n] = {"figur": figur, "datei": f"folge02/figuren/{n}.webp", "x": rahmen[0], "y": rahmen[1],
"b": rahmen[2] - rahmen[0], "h": rahmen[3] - rahmen[1], "box": boxen[n]}
# A5 (Blick links) ist eine Spiegelung von A6 an der Bildmitte: zurückschieben,
# damit die Figur beim Wechsel A6 ↔ A5 nicht seitlich springt.
for n in posen:
if n.startswith(f"{figur}_A5_"):
a6 = boxen[n.replace("A5_blick-links", "A6_blick-rechts")]
posen[n]["x"] += (a6[0] + a6[2]) / 2 - (boxen[n][0] + boxen[n][2]) / 2
print(figur, len(bilder), rahmen)
figuren = {}
for figur, ref in REF.items():
b = posen[ref]["box"]
figuren[figur] = {"refHoehe": b[3] - b[1], "fussX": (b[0] + b[2]) / 2, "fussY": b[3]}
clips = {}
if CLIPS:
for j in sorted(glob.glob(f"{CLIPS}/*.json")):
n = os.path.basename(j)[:-5]
d = json.load(open(j))
bx = d["box"]
clips[n] = {"figur": n.split("_")[0], "datei": f"folge02/clips/{n}.webm",
"x": bx[0] * K_CLIP, "y": bx[1] * K_CLIP, "b": (bx[2] - bx[0]) * K_CLIP,
"h": (bx[3] - bx[1]) * K_CLIP, "frames": d["frames"]}
with open(TS, "w") as f:
f.write("// Automatisch erzeugt von werkzeuge/posen.py – nicht von Hand ändern.\n")
f.write("// Koordinaten in der Bild-Leinwand der Figur (1376 × 768).\n\n")
f.write("export type Pose = {figur: string; datei: string; x: number; y: number; b: number; h: number; box: number[]};\n")
f.write("export type ClipDaten = {figur: string; datei: string; x: number; y: number; b: number; h: number; frames: number};\n\n")
j = lambda o: json.dumps(o, indent="\t")
f.write("export const FIGUREN = " + j(figuren) + " as const;\n\n")
f.write("export const POSEN: Record<string, Pose> = " + j(posen) + ";\n\n")
f.write("export const CLIPS: Record<string, ClipDaten> = " + j(clips) + ";\n")
print("posen.ts", len(posen), "Posen,", len(clips), "Clips")