youtube/schnitt/pipeline/laengen_check.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

142 lines
6.5 KiB
Python

# -*- coding: utf-8 -*-
"""Laengen-Check: Video gegen Audio je Sprache. Schreibt einen Bericht als Textdatei.
A) VOR dem Export (nur Schnittliste + Audio, es wird nichts gerendert):
python -I laengen_check.py --name f01 --schnittliste schnittlisten\\f01.csv --audio-ordner D:\\ton\\f01
B) NACH dem Export (fertige mp4 pruefen: Aufloesung, Bild-/Tonlaenge, Lautheit):
python -I laengen_check.py --name f01 --export-ordner schnitt\\ausgabe\\folgen
(prueft f01_de.mp4, f01_es.mp4, f01_fr.mp4; optional mit --audio-ordner: Quellton gegen Export)
python -I laengen_check.py --datei X.mp4 [Y.mp4 ...]
Bericht: schnitt\\berichte\\laengen_<name>_<zeit>.txt
"""
import argparse
import datetime
import os
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
TOL = 0.15 # s Toleranz Bild/Ton
def vorab(args, sprachen, zeilen, warn):
audio = P.parse_audio_args(args.audio, args.audio_ordner, lambda l: f"{args.name}_{l}", sprachen)
pro_sprache = "{lang}" in args.schnittliste
zeilen.append(f"VORAB-CHECK {args.name}: Schnittliste {args.schnittliste}")
gruppen = {l: [l] for l in sprachen} if pro_sprache else {None: sprachen}
for schluessel, langs in gruppen.items():
liste = args.schnittliste.replace("{lang}", schluessel) if schluessel else args.schnittliste
from folge_exportieren import lese_schnittliste
segs = lese_schnittliste(liste, args.clips_ordner, warn)
_, V, _ = P.bild_graph(segs, "quer")
zeilen.append(f"Bildspur{' ' + schluessel if schluessel else ''}: {len(segs)} Clips, {V:.2f} s ({P.zeit(V)})")
for l in langs:
if l not in audio:
warn.append(f"{l}: keine Audiodatei gefunden.")
continue
a = P.dauer(audio[l])
diff = a - V
urteil = "passt" if abs(diff) <= TOL else (
f"Ton {diff:.1f} s LAENGER als Bild -> Standbild/Verlangsamen noetig" if diff > 0
else f"Ton {-diff:.1f} s KUERZER als Bild -> stiller Schluss")
zeilen.append(f" {l}: Ton {a:.2f} s ({P.zeit(a)}), Differenz {diff:+.2f} s -> {urteil}")
if diff > 3:
warn.append(f"{l}: Bild muesste um {diff:.1f} s verlaengert werden (mehr als 3 s).")
if -diff > 8:
warn.append(f"{l}: Ton {-diff:.1f} s kuerzer als Bild.")
if len(audio) > 1:
al = {l: P.dauer(p) for l, p in audio.items()}
k, g = min(al, key=al.get), max(al, key=al.get)
zeilen.append(f"Laengste Sprache: {g} {al[g]:.1f} s, kuerzeste: {k} {al[k]:.1f} s "
f"(+{(al[g] / al[k] - 1) * 100:.0f} %)")
if al[g] > al[k] * 1.2:
warn.append("Sprachen weichen um mehr als 20 % ab.")
def nachher(dateien, quell_audio, zeilen, warn):
zeilen.append("EXPORT-CHECK")
laengen = {}
for d in dateien:
d = Path(d)
if not d.exists():
warn.append(f"{d} fehlt.")
continue
info = P.probe(d)
v, a = info["video"], info["audio"]
if not v:
warn.append(f"{d.name}: kein Videostream.")
continue
vd = float(v.get("duration") or info["dauer"])
ad = float(a.get("duration") or 0) if a else 0.0
lau = P.messe_lautheit(d) if a else {"lufs": None, "lra": None, "tp": None}
zeilen.append(f"{d.name}: {v['width']}x{v['height']}, {v['r_frame_rate']} fps, Bild {vd:.2f} s, "
f"Ton {ad:.2f} s, {lau['lufs']} LUFS, LRA {lau['lra']}, TP {lau['tp']} dBFS")
laengen[d.stem] = vd
if not a:
warn.append(f"{d.name}: kein Audiostream.")
elif abs(vd - ad) > TOL:
warn.append(f"{d.name}: Bild {vd:.2f} s vs. Ton {ad:.2f} s (Differenz {vd - ad:+.2f} s).")
if lau["lufs"] is not None and abs(lau["lufs"] - P.LUFS_ZIEL) > 1.0:
warn.append(f"{d.name}: Lautheit {lau['lufs']} LUFS (Ziel {P.LUFS_ZIEL:g} +-1).")
if lau["tp"] is not None and lau["tp"] > -1.0:
warn.append(f"{d.name}: True Peak {lau['tp']} dBFS ueber -1.")
if quell_audio:
l = d.stem.rsplit("_", 1)[-1]
q = quell_audio.get(l)
if q:
qd = P.dauer(q)
zeilen.append(f" Quellton {l}: {qd:.2f} s, Export-Ton {ad:.2f} s")
if ad + TOL < qd:
warn.append(f"{d.name}: Export ist {qd - ad:.2f} s KUERZER als der Quellton (Sprache abgeschnitten!).")
werte = list(laengen.values())
if len(werte) > 1 and max(werte) - min(werte) > TOL:
warn.append("Die Sprachfassungen sind unterschiedlich lang: " +
", ".join(f"{k} {v:.1f} s" for k, v in laengen.items()))
def main():
P.init_konsole()
ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
ap.add_argument("--name", default="check")
ap.add_argument("--schnittliste")
ap.add_argument("--export-ordner", dest="export_ordner")
ap.add_argument("--datei", nargs="*")
ap.add_argument("--sprachen", default="de,es,fr")
ap.add_argument("--audio", nargs="*")
ap.add_argument("--audio-ordner", dest="audio_ordner")
ap.add_argument("--clips-ordner", default=str(P.PROJEKT / "clips"), dest="clips_ordner")
args = ap.parse_args()
sprachen = [x.strip().lower() for x in args.sprachen.split(",") if x.strip()]
zeilen, warn = [f"Laengen-Check {datetime.datetime.now():%d.%m.%Y %H:%M}"], []
if args.schnittliste:
vorab(args, sprachen, zeilen, warn)
dateien = list(args.datei or [])
if args.export_ordner:
dateien += [Path(args.export_ordner) / f"{args.name}_{l}.mp4" for l in sprachen]
if dateien:
qa = P.parse_audio_args(args.audio, args.audio_ordner, lambda l: f"{args.name}_{l}", sprachen) \
if (args.audio or args.audio_ordner) else {}
nachher(dateien, qa, zeilen, warn)
if not args.schnittliste and not dateien:
raise P.Fehler("--schnittliste, --export-ordner oder --datei angeben.")
zeilen += ["", "Warnungen:"] + (warn or ["keine"])
P.BERICHTE.mkdir(parents=True, exist_ok=True)
r = P.BERICHTE / f"laengen_{args.name}_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt"
r.write_text("\n".join(zeilen) + "\n", encoding="utf-8")
print("\n".join(zeilen))
print(f"\nBericht: {r}")
sys.exit(1 if warn else 0)
if __name__ == "__main__":
try:
main()
except P.Fehler as e:
print(f"FEHLER: {e}")
sys.exit(1)