# -*- coding: utf-8 -*- """Exportiert eine Folge oder ein Tuerchen (16:9, 1280x720) je Sprache. Gleiche Bildspur fuer alle Sprachen; die Bildlaenge richtet sich nach der laengsten Sprachspur (Standbild am Ende) und es wird gewarnt, wenn die Sprachen zu weit auseinanderliegen. Alle Sprachen werden gleich lang. Aufruf: python -I folge_exportieren.py --name f01 --schnittliste schnittlisten\\f01.csv ^ --audio-ordner D:\\ton\\f01 (erwartet f01_de.wav, f01_es.wav, f01_fr.wav) Eigene Schnittliste je Sprache: --schnittliste schnittlisten\\f01_{lang}.csv Schnittliste (CSV, Semikolon): clip;in_s;out_s;tempo;halten_s;uebergang_s;notiz clip f01_c09, t09_c03, k_05 oder Bereich f01_c08-13 in_s/out_s Ausschnitt im Clip (leer = ganzer Clip) tempo 0,8 bis 1,0 (leer = 1) halten_s Standbild nach dem Clip (z. B. 3 = Mitmachpause) uebergang_s Ueberblendung IN den naechsten Clip (leer = 0,5; max. 1) """ 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 def lese_schnittliste(pfad, clips_root, warn): zeilen = P.lese_csv(pfad) segs = [] for nr, z in enumerate(zeilen, 1): tokens = P.expandiere(z.get("clip", "")) pfade = P.suche_clips(tokens, clips_root) tempo = P.zahl(z.get("tempo"), 1.0) if tempo < 0.8: warn.append(f"Zeile {nr} ({z.get('clip')}): tempo {tempo} < 0,8 (Grenze fuer ruhiges Tempo).") if tempo > 1.0: warn.append(f"Zeile {nr} ({z.get('clip')}): tempo {tempo} > 1 (nicht ruhig).") ue = P.zahl(z.get("uebergang_s"), 0.5) if ue > 1.0: warn.append(f"Zeile {nr}: Ueberblendung {ue} s -> auf 1 s begrenzt.") for t, p in zip(tokens, pfade): segs.append(P.Seg(t, p, in_s=P.zahl(z.get("in_s"), 0.0) or 0.0, out_s=P.zahl(z.get("out_s"), None), tempo=tempo, halten_s=P.zahl(z.get("halten_s"), 0.0) or 0.0, uebergang_s=min(ue, 1.0), modus="quer")) if not segs: raise P.Fehler(f"Schnittliste leer: {pfad}") return segs def main(): P.init_konsole() ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--name", required=True, help="Dateiname ohne Sprache, z. B. f01 oder t09") ap.add_argument("--schnittliste", required=True, help="CSV; {lang} fuer eigene Liste je Sprache") ap.add_argument("--sprachen", default="de,es,fr") ap.add_argument("--audio", nargs="*", help="sprache=datei") ap.add_argument("--audio-ordner", default=None, dest="audio_ordner", help="enthaelt _.wav|mp3|m4a|flac") ap.add_argument("--musik", help="Musik unter der Sprache (wird geloopt)") ap.add_argument("--musik-db", type=float, default=-20.0, dest="musik_db") ap.add_argument("--ohne-ducking", action="store_true", dest="ohne_ducking") ap.add_argument("--clips-ordner", default=str(P.PROJEKT / "clips"), dest="clips_ordner") ap.add_argument("--ausgabe", default=str(P.AUSGABE)) ap.add_argument("--ausblenden", type=float, default=1.0) ap.add_argument("--nachlauf", type=float, default=1.0, help="Bild laeuft so viele s nach dem Ton weiter") ap.add_argument("--max-abweichung", type=float, default=0.20, dest="max_abw", help="Warnung, wenn laengste Sprache so viel (Anteil) laenger ist als die kuerzeste") ap.add_argument("--ueberschreiben", action="store_true") ap.add_argument("--ausserhalb", action="store_true") ap.add_argument("--trocken", action="store_true") args = ap.parse_args() sprachen = [x.strip().lower() for x in args.sprachen.split(",") if x.strip()] audio = P.parse_audio_args(args.audio, args.audio_ordner, lambda l: f"{args.name}_{l}", sprachen) fehlt = [l for l in sprachen if l not in audio] if fehlt: print(f"WARNUNG: kein Audio fuer {', '.join(fehlt)} -> diese Sprache wird nicht exportiert.") sprachen = [l for l in sprachen if l in audio] if not sprachen: raise P.Fehler("Fuer keine Sprache Audio gefunden (--audio oder --audio-ordner).") warn, bericht = [], [] pro_sprache = "{lang}" in args.schnittliste gruppen = {l: [l] for l in sprachen} if pro_sprache else {None: sprachen} alen = {l: P.dauer(audio[l]) for l in sprachen} bericht.append(f"Folge {args.name}, {datetime.datetime.now():%d.%m.%Y %H:%M}") bericht.append("Tonlaengen: " + ", ".join(f"{l} {a:.2f} s ({P.zeit(a)})" for l, a in alen.items())) kurz, lang = min(alen, key=alen.get), max(alen, key=alen.get) if len(alen) > 1 and alen[lang] > alen[kurz] * (1 + args.max_abw): w = (f"Sprachlaengen weichen um {(alen[lang] / alen[kurz] - 1) * 100:.0f} % ab " f"({lang} {alen[lang]:.1f} s vs. {kurz} {alen[kurz]:.1f} s).") if not pro_sprache: w += (" Eine gemeinsame Bildspur passt dann nicht zu allen Sprachen: besser eigene " "Schnittliste je Sprache (--schnittliste ...{lang}.csv) oder Clips an die laengste Sprache anpassen.") warn.append(w) ausdir = P.pruefe_ziel(Path(args.ausgabe) / "folgen", args.ausserhalb) ziele = {} for l in sprachen: z = ausdir / f"{args.name}_{l}.mp4" P.pruefe_ziel(z, args.ausserhalb) P.schreibschutz(z, args.ueberschreiben) ziele[l] = z if not args.trocken: ausdir.mkdir(parents=True, exist_ok=True) temps, ergebnisse = [], [] for schluessel, langs in gruppen.items(): liste = args.schnittliste.replace("{lang}", schluessel) if schluessel else args.schnittliste segs = lese_schnittliste(liste, args.clips_ordner, warn) graph0, V, _ = P.bild_graph(segs, "quer") T = max(V, max(alen[l] for l in langs) + args.nachlauf) extra = T - V tag = f" [{schluessel}]" if schluessel else "" bericht.append(f"Bildspur{tag}: {len(segs)} Clips, {V:.2f} s ({P.zeit(V)}); Ausgabe {T:.2f} s ({P.zeit(T)})") if extra > 0.05: msg = f"Bild{tag} wird um {extra:.2f} s als Standbild am Ende verlaengert." if extra > 3: msg = "WARNUNG " + msg[:-1] + " (mehr als 3 s: Clips verlangsamen / Standbilder an Sprechstellen einplanen)." warn.append(msg) for l in langs: rest = T - alen[l] bericht.append(f" {l}: Ton {alen[l]:.2f} s, Ton endet {rest:.2f} s vor Bildende") if rest > 8: warn.append(f"{l}: Ton endet {rest:.1f} s vor Bildende (andere Sprache ist deutlich laenger).") graph, _, ges = P.bild_graph(segs, "quer", extra_s=extra, ausblenden_s=args.ausblenden) tmp_bild = P.tmp_datei(f"{args.name}_{schluessel or 'alle'}_bild.mp4") temps.append(tmp_bild) print(f"Bildspur{tag} rendern: {len(segs)} Clips, {ges:.1f} s ...") temps.append(P.kodiere_bild([s.pfad for s in segs], graph, tmp_bild, f"{args.name}_{schluessel or 'alle'}", dry=args.trocken)) for l in langs: print(f"Ton {l}: mischen, auf {P.LUFS_ZIEL:g} LUFS normalisieren, exportieren ...") t, _ = P.mische_und_schreibe(tmp_bild, audio[l], args.musik, ges, args.ausblenden, args.musik_db, ziele[l], f"{args.name}_{l}", dry=args.trocken, duck=not args.ohne_ducking) temps += t if not args.trocken: info = P.probe(ziele[l]) lau = P.messe_lautheit(ziele[l]) v = info["video"] z = (f"{ziele[l].name}: {v['width']}x{v['height']}, {info['dauer']:.2f} s ({P.zeit(info['dauer'])}), " f"{lau['lufs']} LUFS, LRA {lau['lra']}, TP {lau['tp']} dBFS") print(" -> " + z) ergebnisse.append(z) P.aufraeumen(temps) bericht += ["", "Ergebnis:"] + ergebnisse bericht += ["", "Warnungen:"] + (warn or ["keine"]) print("\n".join(["", "Warnungen:"] + (warn or ["keine"]))) if not args.trocken: P.BERICHTE.mkdir(parents=True, exist_ok=True) r = P.BERICHTE / f"export_{args.name}_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt" r.write_text("\n".join(bericht) + "\n", encoding="utf-8") print(f"Bericht: {r}") if __name__ == "__main__": try: main() except P.Fehler as e: print(f"FEHLER: {e}") sys.exit(1)