5a6ae2175c
README: 550 -> 308 lines. The dense algorithm prose was moved verbatim to docs/ALGORITHM.md and replaced in the README with a compact "Wenn ein Match falsch wirkt" troubleshooting table and a link. The cutter-facing intro points at the new in-report stills instead of the old HTML report. Cutter report: - Per-side frame rates: trailer timecodes use the trailer file's fps (typically 25), source timecodes use the source file's fps. ffprobe is used to detect each side; falls back to edl_frame_rate if unavailable. - Side-by-side trailer/source preview stills extracted via ffmpeg, taken ~30% into the beat / match window. Stored under output/cutter_stills/ (gitignored). Re-rendered only when the underlying video is newer than the cached jpg. - Compact table at the top, detailed per-beat sections below with the stills inline so the cutter can sight-check phase agreement directly. - New --no-stills flag for fast text-only regeneration. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
309 lines
13 KiB
Markdown
309 lines
13 KiB
Markdown
# 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](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.
|