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>
136 lines
5.9 KiB
Python
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)
|