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

136 lines
5.9 KiB
Python

# -*- coding: utf-8 -*-
"""Prueft katalog_shorts.csv: Format, Regeln, Laengen, fehlende Clips, Abdeckung der Veroeffentlichungs-Slots.
python -I katalog_pruefen.py
python -I katalog_pruefen.py --katalog X.csv --clips-ordner D:\\clips --slots-bis 2026-11-27
Liest nur. Schreibt nichts ausser (optional) einem Bericht nach schnitt\\berichte\\ mit --bericht.
Slots: je Kanal alle 2 Tage 2 Shorts, Start 08.11.2026 (08., 10., 12. ...).
"""
import argparse
import datetime
import os
import re
import sys
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) # noetig bei python -I
import pinolib as P # noqa: E402
PFLICHT = ["id", "clips", "tempo", "start_s", "ende_s", "modus", "titel_de", "titel_es", "titel_fr",
"frueheste_veroeffentlichung"]
def datum(s):
return datetime.date.fromisoformat(s)
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("--clips-ordner", default=str(P.PROJEKT / "clips"), dest="clips_ordner")
ap.add_argument("--slots-start", default="2026-11-08", dest="slots_start")
ap.add_argument("--slots-bis", nargs="*", default=["2026-11-27", "2026-12-10", "2026-12-31"], dest="slots_bis")
ap.add_argument("--bericht", action="store_true")
args = ap.parse_args()
z = P.lese_csv(args.katalog)
out, fehler, warn = [], [], []
if not z:
raise P.Fehler("Katalog leer.")
fehlend_spalten = [c for c in PFLICHT if c not in z[0]]
if fehlend_spalten:
raise P.Fehler("Spalten fehlen: " + ", ".join(fehlend_spalten))
ids, vorhanden, fehlt_clips = set(), 0, set()
gesamt_clips = set()
for r in z:
i = r["id"]
if i in ids:
fehler.append(f"{i}: ID doppelt")
ids.add(i)
try:
tokens = P.expandiere(r["clips"])
tempo = P.zahl(r["tempo"], 1.0)
n = len(tokens)
d = []
for t in tokens:
p = P.clip_pfad(t, args.clips_ordner)
gesamt_clips.add(t)
if p is None:
fehlt_clips.add(t)
d.append(5.0)
else:
d.append(P.dauer(p))
seg = [round(((x) / tempo) * P.FPS) / P.FPS for x in d]
laenge = sum(seg) - (n - 1) * 0.5
ende = P.zahl(r["ende_s"], laenge)
start = P.zahl(r["start_s"], 0.0)
L = min(ende, laenge) - start
if not 20 <= L <= 45:
fehler.append(f"{i}: Laenge {L:.1f} s ausserhalb 20-45 s")
if tempo < 0.8 or tempo > 1.0:
fehler.append(f"{i}: tempo {tempo} ausserhalb 0,8-1,0")
if ende > laenge + 0.3:
warn.append(f"{i}: ende_s {ende} > Clipfolge {laenge:.1f} s")
if r["modus"] not in ("crop", "blur") and "|" not in r["modus"] and "," not in r["modus"]:
fehler.append(f"{i}: modus '{r['modus']}' ungueltig")
for t in tokens:
m = re.match(r"^t(\d+)_c", t)
if m and datum(r["frueheste_veroeffentlichung"]) < datetime.date(2026, 12, int(m.group(1))):
fehler.append(f"{i}: {t} vor Veroeffentlichung von Tuerchen {int(m.group(1))}")
m = re.match(r"^f(\d+)_c", t)
frei = {1: "2026-11-08", 2: "2026-11-15", 3: "2026-11-29"}
if m and int(m.group(1)) in frei and datum(r["frueheste_veroeffentlichung"]) < datum(frei[int(m.group(1))]):
fehler.append(f"{i}: {t} vor Veroeffentlichung von Folge {int(m.group(1))}")
for l in ("de", "es", "fr"):
if not r.get(f"titel_{l}"):
fehler.append(f"{i}: titel_{l} fehlt")
if re.search(r"(?i)winterschlaf|schl[aä]ft ein|nest", r["thema"] + r["titel_de"]) and "Winterschlaf" not in r.get("ton_hinweis", ""):
warn.append(f"{i}: Nest/Schlaf-Thema ohne Winterschlaf-Hinweis in ton_hinweis")
except P.Fehler as e:
fehler.append(f"{i}: {e}")
out.append(f"Katalog: {len(z)} Momente, {len(gesamt_clips)} verschiedene Clips verwendet, "
f"{len(gesamt_clips) - len(fehlt_clips)} davon unter {args.clips_ordner} vorhanden")
if fehlt_clips:
out.append(f"Fehlende Clipdateien: {len(fehlt_clips)} (Laengen mit 5,0 s angenommen)")
start = datum(args.slots_start)
out.append("")
out.append("Abdeckung (je Kanal; 2 Shorts alle 2 Tage ab " + args.slots_start + "):")
for bis in args.slots_bis:
b = datum(bis)
tage = (b - start).days
slots = (tage // 2 + 1) * 2 if tage >= 0 else 0
verf = sum(1 for r in z if datum(r["frueheste_veroeffentlichung"]) <= b)
status = "OK" if verf >= slots else f"FEHLBETRAG {slots - verf}"
out.append(f" bis {bis}: {verf} Momente, {slots} Plaetze -> {status}")
fenster = {}
for r in z:
fenster[r.get("fenster", "?")] = fenster.get(r.get("fenster", "?"), 0) + 1
out.append("Momente je Fenster: " + ", ".join(f"{k}: {v}" for k, v in sorted(fenster.items())))
modi = {}
for r in z:
modi[r["modus"]] = modi.get(r["modus"], 0) + 1
out.append("Modus: " + ", ".join(f"{k}: {v}" for k, v in modi.items()))
out.append("")
out.append("Fehler:" if fehler else "Fehler: keine")
out += [" " + f for f in fehler]
out.append("Hinweise:" if warn else "Hinweise: keine")
out += [" " + w for w in warn]
print("\n".join(out))
if args.bericht:
P.BERICHTE.mkdir(parents=True, exist_ok=True)
pf = P.BERICHTE / f"katalog_{datetime.datetime.now():%Y%m%d_%H%M%S}.txt"
pf.write_text("\n".join(out) + "\n", encoding="utf-8")
print("Bericht:", pf)
sys.exit(1 if fehler else 0)
if __name__ == "__main__":
try:
main()
except P.Fehler as e:
print(f"FEHLER: {e}")
sys.exit(1)