Melbar 2a3840e528 Fix cutter-report clip duration: render full beat / match length
Bug: the report rendered every preview clip at most 3 s, so any beat longer
than 3 s (e.g. beat 02 at 8.56 s) only got a fragment in the cutter report
and looked wrong. The hard 3 s cap was an early prototype constant that
silently truncated normal-length beats.

Trailer clip is now the full beat duration so the cutter sees the entire
reference beat. Source clip is the full matched duration (may be shorter
than the beat when the match drops before the beat ends — that's correct,
the cutter needs to see exactly the matched span).

A 30 s safety cap stays as a guard against runaway durations but it should
never trip on a normal trailer beat.

All existing clips were dropped and re-rendered so the report on disk
matches the new logic.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-04 22:31:40 +02:00
2026-05-02 09:07:41 +02:00
2026-05-02 09:07:41 +02:00
2026-05-02 09:07:41 +02:00
2026-05-02 09:07:41 +02:00

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.

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 im Source 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.

Wichtig: Auch wenn Vision aktiviert ist — der finale Match bleibt CV-verifiziert. Das LLM liefert nur zusätzliche Suchanker.


Voraussetzungen

  • Python 3.11+
  • ffmpeg 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

# Im Projektordner
python -m venv .venv
.\.venv\Scripts\Activate.ps1

# Falls ExecutionPolicy blockiert:
# Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser

2. Abhängigkeiten installieren

pip install -r requirements.txt

3. API-Key konfigurieren

# .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:

[paths]
source_movie      = "B:/Proxy/DeinFilm_FTR.mp4"
reference_trailer = "F:/Encodings/DeinFilm_Trailer.mp4"

Verwendung

# 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):

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.

Log-Level

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

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.

[cv.vibe_check]
top_k_candidates     = 10     # Top-K Kandidaten für Deep Scan
phash_max_distance   = 12     # Hamming-Distanz Schwelle (064)
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.

S
Description
Frame-accurate trailer reconstruction via pure Computer Vision
Readme 218 MiB
Languages
Python 98.7%
PowerShell 1.3%