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>
169 lines
7.4 KiB
Python
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)
|