youtube/schnitt/pipeline/testlauf.py
Lara c27a7ab9f5 Pino-Material aus Laras Chat und Plan-Dokument ins Repo übernehmen
Alle 44 Tabs des Plan-Dokuments (Drehbücher, Sprechtexte DE/ES/FR,
Clip-Prompts, Adventskalender, Kanaltexte, Produktionsliste) als Markdown
unter drehbuecher/, konzept/, ideen/ und veroeffentlichung/. Dazu Projektstand
und Entscheidungen (wissen/), Schnitt-Pipeline, Shorts-Katalog,
Figuren-Referenzbilder und das Kinderkanal-Team als Subagenten
(.claude/agents/). Videos, Startbilder und Ton bleiben außerhalb des Git.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-28 21:50:01 +01:00

169 lines
7.4 KiB
Python

# -*- coding: utf-8 -*-
"""Selbsttest der Pipeline OHNE echte Clips.
Erzeugt Dummy-Clips (testsrc2, 1280x720, 5 s) und Dummy-Audio (sine) in schnitt\\test\\,
rendert je einen Short in crop und blur (3 Sprachen), eine Dummy-Folge in 3 Sprachen,
holt ein letztes Bild und prueft alles mit ffprobe (Aufloesung, Dauer, Lautheit).
python -I testlauf.py
Schreibt nur nach schnitt\\test\\ (und Berichte nach schnitt\\berichte\\).
"""
import os
import subprocess
import sys
from pathlib import Path
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # noetig bei python -I
import pinolib as P # noqa: E402
T = P.SCHNITT / "test"
PY = [sys.executable, "-I"]
PIPE = P.SCHNITT / "pipeline"
ok_alle = True
zeilen = []
def log(s=""):
print(s)
zeilen.append(s)
def ff(args):
P.ffmpeg_lauf(args, was="Testdatei")
def dummy_clip(pfad, nr):
if pfad.exists():
return
pfad.parent.mkdir(parents=True, exist_ok=True)
ff(["-f", "lavfi", "-i", f"testsrc2=size=1280x720:rate=24:duration=5",
"-vf", f"hue=h={(nr * 29) % 360},format=yuv420p", "-c:v", "libx264", "-crf", "20",
"-pix_fmt", "yuv420p", str(pfad)])
def dummy_audio(pfad, sek, freq, db, stereo=False):
if pfad.exists():
return
pfad.parent.mkdir(parents=True, exist_ok=True)
quelle = f"sine=frequency={freq}:duration={sek}:sample_rate=44100,tremolo=f=3:d=0.6,volume={db}dB"
ff(["-f", "lavfi", "-i", quelle, "-ac", "2" if stereo else "1", str(pfad)])
def starte(name, args):
log(f"$ {name}")
r = subprocess.run(PY + [str(PIPE / name)] + [str(a) for a in args], capture_output=True)
out = (r.stdout.decode("utf-8", "replace") + r.stderr.decode("utf-8", "replace")).strip()
for z in out.splitlines():
log(" " + z)
return r.returncode
def pruefe(bedingung, text):
global ok_alle
log((" OK " if bedingung else " FEHL ") + text)
ok_alle = ok_alle and bool(bedingung)
def main():
P.init_konsole()
clips = T / "clips"
# --- Dummy-Material
for i in range(1, 13):
dummy_clip(clips / "folge01" / f"f01_c{i:02d}.mp4", i)
dummy_clip(clips / "bausteine" / "k_05.mp4", 5)
ton = T / "ton"
for l, f, d in (("de", 220, -26), ("es", 247, -24), ("fr", 262, -28)):
dummy_audio(ton / "shorts" / f"T001_{l}.wav", {"de": 24, "es": 27, "fr": 25}[l], f, d, stereo=(l == "es"))
dummy_audio(ton / "shorts" / f"T002_{l}.wav", {"de": 26, "es": 30, "fr": 28}[l], f, d)
dummy_audio(ton / "f01" / f"f01_{l}.wav", {"de": 52, "es": 61, "fr": 57}[l], f, d, stereo=(l == "es"))
dummy_audio(ton / "shorts" / "musik.wav", 20, 110, -10, stereo=True)
(T / "katalog_test.csv").write_text(
"id;quelle;clips;tempo;start_s;ende_s;modus;crop_x;musik;thema\n"
"T001;Dummy;f01_c01-05;1.0;0;;crop;0.3|0.5|0.7|0.5|0.5;;Crop-Test (5 Clips)\n"
"T002;Dummy;f01_c06-10;0,85;0;;blur;;musik.wav;Blur-Test mit Musik (5 Clips, Tempo 0,85)\n",
encoding="utf-8-sig")
(T / "schnittliste_f01.csv").write_text(
"clip;in_s;out_s;tempo;halten_s;uebergang_s;notiz\n"
"f01_c01;;;;;1.0;Buch auf, lange Blende\n"
"f01_c02;;;0.8;3;0.5;verlangsamt + 3 s Mitmachpause\n"
"f01_c03-05;;;;;0.5;Bereich\n"
"k_05;;;;;0.5;Baustein\n"
"f01_c06;1;4;1;0;0.5;nur Sekunde 1-4\n"
"f01_c07-12;;;;;0.5;\n", encoding="utf-8-sig")
gemeinsam = ["--clips-ordner", clips, "--ausgabe", T / "ausgabe", "--ueberschreiben"]
log("== 1. Short crop + blur, 3 Sprachen")
rc = starte("short_rendern.py", ["--katalog", T / "katalog_test.csv", "--id", "T001,T002",
"--audio-ordner", ton / "shorts", "--musik-db", "-20"] + gemeinsam)
pruefe(rc == 0, "short_rendern beendet ohne Fehler")
erwartet = {"T001": {"de": 24, "es": 27, "fr": 25}, "T002": {"de": 26, "es": 30, "fr": 28}}
for sid, d in erwartet.items():
for l, a in d.items():
f = T / "ausgabe" / "shorts" / f"{sid}_{l}.mp4"
if not f.exists():
pruefe(False, f"{f.name} fehlt")
continue
i = P.probe(f)
v, au = i["video"], i["audio"]
lau = P.messe_lautheit(f)
soll = max({"T001": 23.0, "T002": 27.4}[sid], a + 0.8) # Clipfolge oder Ton + Nachlauf
pruefe((v["width"], v["height"]) == (1080, 1920), f"{f.name}: Aufloesung {v['width']}x{v['height']}")
pruefe(abs(i["dauer"] - soll) < 0.3, f"{f.name}: Dauer {i['dauer']:.2f} s (soll ca. {soll:.1f})")
pruefe(au is not None and au["sample_rate"] == "48000", f"{f.name}: Audio {au['codec_name']} {au['sample_rate']} Hz")
pruefe(lau["lufs"] is not None and abs(lau["lufs"] + 14) <= 1.0,
f"{f.name}: Lautheit {lau['lufs']} LUFS, TP {lau['tp']} dBFS")
log("== 2. Folge f01 in 3 Sprachen (gemeinsame Bildspur, laengste Sprache fuehrt)")
rc = starte("folge_exportieren.py", ["--name", "f01", "--schnittliste", T / "schnittliste_f01.csv",
"--audio-ordner", ton / "f01"] + gemeinsam)
pruefe(rc == 0, "folge_exportieren beendet ohne Fehler")
dauern = []
for l in P.SPRACHEN:
f = T / "ausgabe" / "folgen" / f"f01_{l}.mp4"
if not f.exists():
pruefe(False, f"{f.name} fehlt")
continue
i = P.probe(f)
v = i["video"]
lau = P.messe_lautheit(f)
dauern.append(i["dauer"])
pruefe((v["width"], v["height"]) == (1280, 720), f"{f.name}: Aufloesung {v['width']}x{v['height']}")
pruefe(abs(i["dauer"] - 62.0) < 0.5, f"{f.name}: Dauer {i['dauer']:.2f} s (laengster Ton es 61 s + 1 s Nachlauf)")
pruefe(abs(lau["lufs"] + 14) <= 1.0, f"{f.name}: Lautheit {lau['lufs']} LUFS, TP {lau['tp']} dBFS")
if dauern:
pruefe(max(dauern) - min(dauern) < 0.1, "alle Sprachen gleich lang")
log("== 3. Letztes Bild")
rc = starte("letztes_bild.py", ["--clip", "f01_c04"] + gemeinsam)
png = T / "ausgabe" / "startbilder" / "f01_c04_ende.png"
pruefe(rc == 0 and png.exists() and png.stat().st_size > 10000, f"PNG vorhanden ({png.stat().st_size if png.exists() else 0} Bytes)")
if png.exists():
v = P.probe(png)["video"]
pruefe((v["width"], v["height"]) == (1280, 720), f"PNG-Aufloesung {v['width']}x{v['height']}")
log("== 4. Laengen-Check (vorab + nach Export)")
rc = starte("laengen_check.py", ["--name", "f01", "--schnittliste", T / "schnittliste_f01.csv",
"--audio-ordner", ton / "f01", "--clips-ordner", clips,
"--export-ordner", T / "ausgabe" / "folgen"])
pruefe(rc in (0, 1), "laengen_check lief durch (Warnungen sind erwartet: Sprachen weichen ab)")
log("== 5. Schutz: Ausgabe ausserhalb schnitt\\ wird verweigert")
rc = starte("short_rendern.py", ["--katalog", T / "katalog_test.csv", "--id", "T001",
"--audio-ordner", ton / "shorts", "--clips-ordner", clips,
"--ausgabe", P.PROJEKT / "shorts"])
pruefe(rc != 0 and not (P.PROJEKT / "shorts" / "shorts").exists(), "Ablehnung ohne --ausserhalb")
log()
log("GESAMT: " + ("ALLES OK" if ok_alle else "FEHLER VORHANDEN"))
(T / "testergebnis.txt").write_text("\n".join(zeilen) + "\n", encoding="utf-8")
sys.exit(0 if ok_alle else 1)
if __name__ == "__main__":
try:
main()
except P.Fehler as e:
print(f"FEHLER: {e}")
sys.exit(1)