# -*- coding: utf-8 -*- """Rendert Shorts (1080x1920) je Sprache aus einer Zeile des Moment-Katalogs. Aufruf (immer mit -I): python -I short_rendern.py --id S001 --audio-ordner D:\\ton\\shorts python -I short_rendern.py --id S001,S002 --sprachen de,es python -I short_rendern.py --alle --audio-ordner ... (alle Zeilen mit Status offen) python -I short_rendern.py --clips "f01_c08-13" --modus blur --name probe --audio de=a.wav es=b.wav fr=c.wav Audio je Sprache: \\_.wav|mp3|m4a|flac (S001_de.wav ...) Ausgabe: \\shorts\\_.mp4 (Standard: schnitt\\ausgabe) """ import argparse import os import sys sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # noetig bei python -I import pinolib as P # noqa: E402 def segmente_aus_zeile(z, clips_root): tokens = P.expandiere(z["clips"]) if not tokens: raise P.Fehler("Spalte 'clips' ist leer.") pfade = P.suche_clips(tokens, clips_root) n = len(tokens) tempo = P.zahl(z.get("tempo"), 1.0) if tempo < 0.8 or tempo > 1.0: print(f" WARNUNG: tempo {tempo} ausserhalb 0,8-1,0 (ruhiges Tempo).") modi = P.auf_laenge(P.liste_texte(z.get("modus"), "blur"), n, "modus") cxs = P.auf_laenge(P.liste_zahlen(z.get("crop_x"), 0.5), n, "crop_x") for m in modi: if m not in ("crop", "blur"): raise P.Fehler(f"modus muss crop oder blur sein, nicht '{m}'.") uebergang = P.zahl(z.get("uebergang_s"), 0.5) return [P.Seg(t, p, tempo=tempo, uebergang_s=uebergang, modus=m, crop_x=c) for t, p, m, c in zip(tokens, pfade, modi, cxs)] def render_short(kid, segs, start, ende, audio, musik, args, bericht): from pathlib import Path ausdir = P.pruefe_ziel(Path(args.ausgabe) / "shorts", args.ausserhalb) sprachen = [l for l in args.sprachen if l in audio] fehlend = [l for l in args.sprachen if l not in audio] if fehlend: bericht.append(f"{kid}: kein Audio fuer {', '.join(fehlend)} -> diese Sprache uebersprungen.") print(" " + bericht[-1]) if not sprachen: raise P.Fehler(f"{kid}: fuer keine Sprache Audio vorhanden.") # Bildlaenge bestimmen (Probe-Graph ohne Extra) _, gesamt_probe_laenge, _ = P.bild_graph(segs, "hoch") ende_e = min(ende or gesamt_probe_laenge, gesamt_probe_laenge) start_e = start or 0.0 if ende and ende > gesamt_probe_laenge + 0.05: bericht.append(f"{kid}: ende_s {ende} > Clipfolge {gesamt_probe_laenge:.2f} s -> gekuerzt.") L = ende_e - start_e alen = {l: P.dauer(audio[l]) for l in sprachen} # Jede Sprache bekommt ihre eigene Laenge (eigener Kanal, eigener Upload): # Bild = Clipfolge, bei laengerem Ton wird das letzte Bild gehalten. Tl = {l: max(L, alen[l] + args.nachlauf) for l in sprachen} msgs = [f"{kid}: Bild {L:.2f} s, Ton " + ", ".join(f"{l} {a:.2f} s" for l, a in alen.items()) + " -> Ausgabe " + ", ".join(f"{l} {Tl[l]:.2f} s" for l in sprachen)] for l in sprachen: extra = Tl[l] - L if extra > 0.05: msgs.append(f"{kid} {l}: Bild wird {extra:.2f} s als Standbild gehalten" + (" (WARNUNG: mehr als 3 s - Clips verlangsamen oder anderes Stueck waehlen)" if extra > 3 else "")) if not 20 <= Tl[l] <= 45: msgs.append(f"{kid} {l}: WARNUNG Laenge {Tl[l]:.1f} s ausserhalb 20-45 s.") if L - alen[l] > 4: msgs.append(f"{kid} {l}: WARNUNG Bild endet {L - alen[l]:.1f} s nach dem Ton (stiller Schluss).") for m in msgs: print(" " + m) bericht += msgs # Zielpfade pruefen, bevor irgendetwas gerendert wird ziele = {} for l in sprachen: z = ausdir / f"{kid}_{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, bilder = [], {}, {} for l in sprachen: extra = max(0.0, Tl[l] - L) key = round(extra, 2) if key not in bilder: graph, _, _ = P.bild_graph(segs, "hoch", start=start_e, ende=ende_e, extra_s=extra, ausblenden_s=args.ausblenden) tmp_bild = P.tmp_datei(f"{kid}_bild_{len(bilder)}.mp4") print(f" Bildspur rendern ({len(segs)} Clips, {segs[0].modus}, {L + extra:.1f} s) ...") temps.append(tmp_bild) temps.append(P.kodiere_bild([s.pfad for s in segs], graph, tmp_bild, f"{kid}_v{len(bilder)}", dry=args.trocken)) bilder[key] = tmp_bild print(f" Ton {l}: mischen, auf {P.LUFS_ZIEL:g} LUFS normalisieren ...") t, mess = P.mische_und_schreibe(bilder[key], audio[l], musik, L + extra, args.ausblenden, args.musik_db, ziele[l], f"{kid}_{l}", dry=args.trocken, duck=not args.ohne_ducking) temps += t if not args.trocken: info = P.probe(ziele[l]) v = info["video"] lau = P.messe_lautheit(ziele[l]) zeile = (f"{ziele[l].name}: {v['width']}x{v['height']}, {info['dauer']:.2f} s, " f"{lau['lufs']} LUFS, TP {lau['tp']} dBFS") print(" -> " + zeile) bericht.append(zeile) ergebnisse[l] = (ziele[l], info, lau) P.aufraeumen(temps) return ergebnisse def main(): P.init_konsole() ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) ap.add_argument("--katalog", default=str(P.SCHNITT / "katalog_shorts.csv")) ap.add_argument("--id", help="Katalog-ID(s), Komma getrennt") ap.add_argument("--alle", action="store_true", help="alle Katalogzeilen (nur mit Status offen)") ap.add_argument("--clips", help="statt Katalog: Clipliste, z. B. f01_c08-13") ap.add_argument("--name", default="probe", help="Name bei --clips") ap.add_argument("--modus", default="blur", help="crop|blur (bei --clips), mehrere mit |") ap.add_argument("--crop-x", default="0.5", dest="crop_x") ap.add_argument("--tempo", default="1.0") ap.add_argument("--start", type=float, default=None) ap.add_argument("--ende", type=float, default=None) ap.add_argument("--sprachen", default="de,es,fr") ap.add_argument("--audio", nargs="*", help="sprache=datei (z. B. de=a.wav es=b.wav)") ap.add_argument("--audio-ordner", default=str(P.PROJEKT / "ton" / "shorts")) ap.add_argument("--musik", help="Musikdatei (leise darunter, 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, help="Ausblendzeit am Ende in s (0 = aus)") ap.add_argument("--nachlauf", type=float, default=0.8, help="Bild laeuft so viele s nach dem Ton weiter") ap.add_argument("--ueberschreiben", action="store_true") ap.add_argument("--ausserhalb", action="store_true", help="Ausgabe ausserhalb von schnitt\\ erlauben") ap.add_argument("--trocken", action="store_true", help="nur anzeigen, nichts rendern") args = ap.parse_args() args.sprachen = [x.strip().lower() for x in args.sprachen.split(",") if x.strip()] zeilen = [] if args.clips: zeilen.append({"id": args.name, "clips": args.clips, "modus": args.modus, "crop_x": args.crop_x, "tempo": args.tempo, "start_s": "" if args.start is None else str(args.start), "ende_s": "" if args.ende is None else str(args.ende), "musik": ""}) else: kat = P.lese_csv(args.katalog) if args.id: wanted = [x.strip() for x in args.id.split(",")] by = {z["id"]: z for z in kat} miss = [w for w in wanted if w not in by] if miss: raise P.Fehler(f"ID nicht im Katalog: {', '.join(miss)}") zeilen = [by[w] for w in wanted] elif args.alle: zeilen = [z for z in kat if all(z.get(f"status_{l}", "offen") in ("", "offen") for l in args.sprachen)] else: raise P.Fehler("--id, --alle oder --clips angeben.") bericht, fehler = [], 0 for z in zeilen: kid = z["id"] print(f"== {kid}: {z.get('thema', '')}") try: segs = segmente_aus_zeile(z, args.clips_ordner) audio = P.parse_audio_args(args.audio, args.audio_ordner, lambda l, k=kid: f"{k}_{l}", args.sprachen) musik = args.musik or (z.get("musik") or None) if musik and not os.path.isabs(musik): musik = os.path.join(args.audio_ordner, musik) render_short(kid, segs, P.zahl(z.get("start_s")), P.zahl(z.get("ende_s")), audio, musik, args, bericht) except P.Fehler as e: fehler += 1 print(f" FEHLER: {e}") bericht.append(f"{kid}: FEHLER {e}") if not args.trocken and bericht: import datetime P.BERICHTE.mkdir(parents=True, exist_ok=True) r = P.BERICHTE / f"shorts_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt" r.write_text("\n".join(bericht) + "\n", encoding="utf-8") print(f"Bericht: {r}") sys.exit(1 if fehler else 0) if __name__ == "__main__": try: main() except P.Fehler as e: print(f"FEHLER: {e}") sys.exit(1)