# -*- 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__.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)