# AI Trailer Generator v2 **Frame-genaues Nachbauen eines Trailers aus dem Quellfilm.** Du gibst zwei Videos rein — einen Referenz-Trailer und den dazugehörigen Spielfilm — und bekommst eine fertige FCPXML/EDL für deinen Schnittplatz, die den Trailer Beat für Beat aus dem Quellfilm nachbaut. --- ## Für den Cutter — was du wirklich brauchst Du musst dieses Tool **nicht selbst bedienen** und musst **kein Python können**. Was du bekommst sind zwei Dateien, mit denen du arbeitest: 1. **`CUTTER_REPORT.md`** — die Tabelle für die manuelle Kontrolle und das Nachschneiden. Pro Beat steht drin: - der Trailer-Zeitcode (h:mm:ss:ff), - der vorgeschlagene Source-Zeitcode aus dem Spielfilm, - ein Status: `OK` (kann übernommen werden), `?` (bitte sichten) oder `MAN.` (kein Treffer, manuell setzen), - eine kurze Beschreibung, was im Trailer-Beat zu sehen ist (damit du die richtige Stelle im Source schneller findest). 2. **`output/*.fcpxml`** und **`output/*.edl`** — die fertige Timeline für FCP / Premiere / Avid / Resolve. Beats mit Status `OK` sind dort schon richtig gesetzt; `?` und `MAN.` musst du im NLE prüfen bzw. selbst setzen. **Workflow-Empfehlung:** 1. Öffne `CUTTER_REPORT.md` und arbeite die Tabelle von oben nach unten ab. 2. Importiere die FCPXML/EDL ins NLE, lade Trailer und Spielfilm dazu. 3. Bei `OK`-Beats nur stichprobenartig sichten. 4. Bei `?`-Beats die beiden Vorschau-Stills im `CUTTER_REPORT.md` direkt vergleichen (Trailer-Frame neben Source-Frame). Wenn die Bewegungsphase nicht passt, im NLE den Source-In um wenige Frames vor/zurück verschieben. 5. Bei `MAN.`-Beats selbst die passende Stelle im Spielfilm suchen — die Beschreibung im Report sagt dir was du suchst. Für die visuelle Kontrolle ist zusätzlich **`CUTTER_REPORT.html`** relevant: er enthält die frame-locked Compare-Clips. Der alte `match_report.html` ist nicht mehr Teil des Workflows. Alles andere unten ist Hintergrund für den Tool-Verantwortlichen. --- ## Wie das Tool die Treffer findet (Kurzfassung) | Phase | Was passiert | |-------|--------------| | **0** | Trailer in Beats zerlegen (PySceneDetect). | | **1** | Schneller Vibe-Check: für jeden Beat die Top-K ähnlichsten Szenen aus dem Spielfilm vorauswählen (Histogramm + pHash). | | **2** | Optional: Vision-LLM beschreibt unsichere Szenen mit 3-Frame-Samples; die Beschreibungen liegen gecached vor. | | **3** | Frame-genaue Verfeinerung pro Beat (OpenCV-Templatematching, Bewegungsphasen-Vergleich). | | **4** | Phasen-Reparatur: bei segmentierten Beats wird die Bewegungsphase lokal um den gefundenen Inpoint saliency- und motion-gewichtet mit der sichtbaren Trailerphase abgeglichen. | | **5** | Recovery: Beats ohne Treffer werden via Vision-Phasensuche in den Top-K Szenen nochmal probiert. | | **6** | Export als FCPXML 1.10 oder CMX-3600-EDL plus `CUTTER_REPORT.md`. | **Text-Safe Crop:** Obere 15 % und untere 30 % jedes Frames werden vor dem Vergleich ausgeblendet, damit Title-Cards, Logos und Letterbox die Treffer nicht verfälschen. **Cutter-Report-Caching:** Vorhandene Compare-Clips werden wiederverwendet. Bei gezielten Rematches wird nur der betroffene Beat neu gerendert, damit der Report schnell aktuell bleibt und keine unnötigen Videoartefakte neu entstehen. **Wichtig:** Auch wenn Vision aktiviert ist — der finale Match bleibt CV-verifiziert. Das LLM liefert nur zusätzliche Suchanker. --- ## Voraussetzungen - Python **3.11+** - [ffmpeg](https://ffmpeg.org/download.html) im PATH (für Whisper Audio-Extraktion) - CUDA-fähige GPU empfohlen (für faster-whisper; CPU funktioniert auch) --- ## Setup ### 1. Virtual Environment erstellen & aktivieren ```powershell # Im Projektordner python -m venv .venv .\.venv\Scripts\Activate.ps1 # Falls ExecutionPolicy blockiert: # Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser ``` ### 2. Abhängigkeiten installieren ```powershell pip install -r requirements.txt ``` ### 3. API-Key konfigurieren ```powershell # .env aus dem Template kopieren Copy-Item .env.example .env # Dann .env öffnen und den echten Key eintragen: # OPENROUTER_API_KEY=sk-or-v1-... ``` ### 4. Videodateien eintragen `config.toml` öffnen und die Pfade anpassen: ```toml [paths] source_movie = "B:/Proxy/DeinFilm_FTR.mp4" reference_trailer = "F:/Encodings/DeinFilm_Trailer.mp4" ``` --- ## Verwendung ```powershell # Vollständige Pipeline (analyze → match → report → export) python cli.py run # Ohne Whisper-Transkription (schneller) python cli.py run --no-audio # Ohne LLM-Klassifikation python cli.py run --no-audio --no-llm # Schrittweise python cli.py analyze # Reference Trailer → Beats erkennen python cli.py match # Globaler FFmpeg Scan (Szenen-unabhängig) python cli.py report # HTML Report mit Video-Vergleich bauen python cli.py export --format both # FCPXML + EDL ausgeben # Gezielt nur einen Beat bearbeiten (empfohlen für erste Iterationen) python cli.py match --beat 5 python cli.py match --beat 5 --vision # optionale gecachte Vision-Seeds python cli.py report --beat 5 python cli.py export --beat 5 --format both # Fehlerhafte Matches korrigieren python cli.py rematch --beat 5 --threshold 0.50 # Schwelle anpassen (Globaler Scan wird für diesen Beat wiederholt) python cli.py rematch --beat 5 --refine # Cached Match per lokalem Bildinhalt-Offset nachschärfen ``` ### Cutter-Report neu erzeugen `CUTTER_REPORT.md` wird bei jedem `match`-Lauf automatisch geschrieben. Manuell neu erzeugen (z. B. nach Edit eines einzelnen Beats): ```powershell python scripts/generate_cutter_report.py # mit Vorschau-Stills python scripts/generate_cutter_report.py --no-stills # nur die Tabelle ``` Stills landen unter `output/cutter_stills/` und werden nur neu gerendert, wenn sich das zugrundeliegende Match geändert hat. ### Wenn ein Match falsch wirkt | Symptom | Was tun | |---------|---------| | Source-Clip zeigt richtige Szene, aber falsche Bewegungsphase | `python cli.py rematch --beat N --refine` — schiebt den Inpoint frame-genau aus dem Bildinhalt. | | Score zu niedrig, andere Szene wäre richtig | `python cli.py match --beat N --vision` — vollständiger Re-Match nur für diesen Beat mit Vision-Phasenprüfung. | | Match offensichtlich falsche Szene | `python cli.py rematch --beat N --threshold 0.50` — Schwelle absenken, neuer globaler Scan nur für diesen Beat. | | Beat ist Schwarzbild / Logo / Titel und sollte gar nicht matchen | nichts tun, der Status `MAN.` im `CUTTER_REPORT.md` ist korrekt. | ### Algorithmische Details Tiefer in die Matcher-Logik (Phasen-Reparatur, segmentierte Beats, Vision-Seeds, Recovery-Stufe usw.) — siehe [`docs/ALGORITHM.md`](docs/ALGORITHM.md). ### Log-Level ```powershell python cli.py run --log-level DEBUG ``` --- ## Projektstruktur ``` ai_trailer_2026/ │ ├── config.toml ← Alle Parameter (kein Hardcoding im Code) ├── .env ← API-Keys (NICHT commiten) ├── cli.py ← Einstiegspunkt │ ├── src/ │ ├── core/ │ │ ├── config.py load_config() → AppConfig (frozen dataclasses) │ │ └── models.py Scene, TrailerBeat, VibeHit, MatchResult, EditTimeline │ ├── cv/ │ │ ├── fingerprinting.py Text-Safe Crop · HS-Histogramme · pHash │ │ ├── vibe_check.py Phase 1: Histogram+pHash Filter │ │ ├── scene_indexer.py PySceneDetect → Fingerprint → JSON-Cache │ │ ├── frame_extractor.py VideoCapture-Wrapper │ │ └── deep_scan.py Phase 2: Coarse+Refine Template-Matching │ ├── audio/ │ │ └── transcriber.py faster-whisper Transkription │ ├── llm/ │ │ ├── dramaturg.py OpenRouter → BeatType (Dialog/Dramaturgie) │ │ └── vision_cache.py optionale gecachte 3-Frame Vision-Seeds │ ├── pipeline/ │ │ ├── trailer_analyzer.py Reference-Trailer → TrailerBeat[] │ │ └── matcher.py Orchestrierung + EditTimeline-Builder │ └── export/ │ ├── timecode.py Sekunden ↔ FCPXML-Rational ↔ SMPTE │ ├── fcpxml_writer.py FCPXML 1.10 │ └── edl_writer.py CMX 3600 EDL │ ├── output/ ← FCPXML/EDL Output (gitignored) ├── .cache/ ← Szenen-Index + Match-Ergebnisse (gitignored) └── tests/ 52 Unit-Tests (pytest) ``` --- ## Cache-Verhalten Damit nicht bei jedem Lauf der gesamte Quellfilm neu analysiert werden muss: | Datei | Inhalt | Neu bauen mit | |-------|--------|---------------| | `.cache/scene_index.json` | Alle Quellfilm-Szenen + Fingerprints | `--force-reindex` | | `.cache/trailer_beats.json` | Erkannte Trailer-Beats | `python cli.py analyze` erneut | | `.cache/match_results.json` | CV-Matching-Ergebnisse | `python cli.py match` erneut | | `.cache/vision_descriptions.json` | Optionale 3-Frame Vision-Beschreibungen für Beats/Szenen | löschen oder anderes Vision-Modell konfigurieren | --- ## Tests ```powershell pytest tests/ -v ``` Alle Tests laufen ohne echte Videodateien (synthetische Frames via numpy/OpenCV). --- ## Konfiguration (Auszug) Alle Werte in `config.toml` — keine hardgecodeten Konstanten im Code. ```toml [cv.vibe_check] top_k_candidates = 10 # Top-K Kandidaten für Deep Scan phash_max_distance = 12 # Hamming-Distanz Schwelle (0–64) crop_top_fraction = 0.15 # Obere 15% ausblenden (Logos) crop_bottom_fraction = 0.30 # Untere 30% ausblenden (Letterbox/Subs) [cv.deep_scan] coarse_step_seconds = 0.5 # Scan-Schrittgröße (Coarse Pass) match_threshold = 0.65 # Mindestscore für bestätigte automatische Matches provisional_match_threshold = 0.43 # Niedrigere automatische Kandidaten im Report zeigen coarse_candidate_threshold = 0.50 # Niedrigeres Gate vor Multi-Frame-Refine refine_window_seconds = 0.6 # Suchfenster für framegenaue Inpoint-Feinjustage refine_step_seconds = 0.04 # ~1 Frame bei 25fps (Refine Pass) content_align_window_seconds = 0.48 # Lokales Suchfenster um einen groben Treffer content_align_sample_step_s = 0.28 # Referenzframes für direkten Bildinhalt-Offset content_validation_weight = 0.35 # Gewicht der festen Whole-Frame-/Spatial-Endprüfung provisional_content_threshold = 0.42 # Untergrenze für Report-/Cache-Kandidaten start_tie_break_score_delta = 0.015 # Bei fast gleichen Scores früheren Inpoint wählen start_preroll_frames = 0 # Kein pauschaler Start-Ausgleich; Offset kommt aus Bildinhalt sequence_candidate_count = 240 # Breiter Kandidatenpool vor Inhalts-Reranking max_refine_candidates = 6 # Teurer Frame-Refine läuft nur auf den besten Inhaltskandidaten scene_seed_top_k = 30 # Scene-Level-Kandidaten als zusätzliche Suchanker scene_seed_points_per_scene = 6 # Inpoint-Samples pro Scene-Level-Kandidat content_rerank_candidate_count = 100 # Grobe Kandidaten vor Inhalts-Reranking skip_coarse_scan_with_weighted_seeds = false # Vision-Seeds nur als Hinweise; Vollscan bleibt robust sequence_score_weight = 0.55 # Gewicht für mehrere zeitliche Vergleichsframes span_score_weight = 0.15 # Gewicht für Stabilität bis zum Beat-Ende coarse_score_weight = 0.10 # Gewicht des groben Midpoint-Treffers duration_score_weight = 0.20 # Gewicht für nutzbare Länge des Source-Treffers duration_tie_break_score_delta = 0.03 # Bei ähnlichem Score längeren Treffer bevorzugen min_duration_coverage = 0.65 # Treffer muss mindestens 65% des matchbaren Referenzanteils tragen continuity_seed_offsets_s = [-1.0, 0.0, 0.5, 1.0, 1.5, 2.0, 3.0] # Suchanker um gematchte Nachbarbeats span_sample_step_s = 0.08 # Schrittweite für End-/Drift-Erkennung trim_tail_frames = 4 # Sicherheitsabstand gegen kurze Blitzer am Ende scene_boundary_epsilon_s = 0.12 # Szenengrenzen-Toleranz gegen 1-2 Frame Cut-Drift scoreable_luma_mean_min = 24.0 # Zu dunkle/Fade-Frames nicht scoren scoreable_luma_p90_min = 58.0 # Helle Bildanteile müssen sichtbar genug sein scoreable_contrast_min = 24.0 # Kontrastarme Blenden/Titelinseln ignorieren [vision] enabled = false # Kostenkontrolle: per CLI mit --vision aktivierbar model = "google/gemma-4-31b-it" # Muss ein visionfähiges OpenAI-kompatibles Modell sein scene_candidate_top_k = 48 # Breiter Vision-Kandidatenpool für schwierige Beats max_new_descriptions_per_run = 24 # Gecachte Beschreibungen pro Lauf; Rate-Limits bekommen Backoff max_seed_scenes = 8 # Mehr Vision-Szenen als Suchanker, kein manueller Override seed_points_per_scene = 12 # Inpoint-Samples pro Vision-Szene seed_score = 0.88 # Vision-Seeds bekommen mehr Priorität als normale Scene-Seeds max_refine_candidates = 12 # Vision-Pfad prüft mehrere Bewegungsphasen derselben Szene local_scan_step_s = 0.12 # Dichte lokale Bildsuche in Vision-Szenen local_scan_max_points_per_scene = 180 # Laufzeitgrenze pro Source-Szene local_scan_top_candidates = 36 # Beste lokale Kandidaten gehen ins Refinement local_scan_tie_break_score_delta = 0.08 # Ähnliche Vision-Treffer: frühere Phase bevorzugen multi_shot_cut_corr_threshold = 0.20 # Interne Trailer-Umschnitte erkennen multi_shot_boundary_tolerance_s = 0.20 # Source-Grenze muss zum Trailer-Cut passen fullscan_fallback = false # Nur relevant, wenn skip_coarse_scan_with_weighted_seeds=true ist content_threshold = 0.22 # Lockeres Content-Gate nur für gewichtete Vision-Seeds similarity_threshold = 0.18 # Mindest-Textähnlichkeit für Vision-Seeds ``` --- ## Lizenz Internes Tool — nicht für den öffentlichen Vertrieb.