97a8f9e305
- New CUTTER_REPORT.md: per-beat hand-off table for the video editor doing the manual recut. Per beat: trailer SMPTE in/out, source SMPTE in/out, scene id, score, status (OK / ? / MAN.), and a one-line phase description from the cached vision text. - New scripts/generate_cutter_report.py: pure renderer that reads the current cache (match_results.json + trailer_beats.json + optional vision_descriptions.json) and writes CUTTER_REPORT.md. No side effects on the cache. - cli.py: after every successful match the cutter report is regenerated automatically (best-effort; failures are logged and do not abort). - README.md: new top-section "Fuer den Cutter" describing exactly what the editor needs (which two files to look at, how the status flag works, the recommended NLE workflow). The technical algorithm description follows below. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
551 lines
30 KiB
Markdown
551 lines
30 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 den Vorschauclip aus dem Report-HTML (siehe unten) prüfen
|
||
und im NLE den Source-In um wenige Frames vor/zurück verschieben, bis die
|
||
Bewegungsphase exakt zum Trailer passt.
|
||
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
|
||
```
|
||
|
||
Der HTML-Report regeneriert seine Preview-Clips bei jedem Lauf mit genauer
|
||
FFmpeg-Nachsuche und synchronisiert die beiden Video-Player pro Beat. Dadurch
|
||
ist der Report zur Frame-Prüfung geeignet und zeigt keine alten gecachten
|
||
Preview-Clips.
|
||
Source-Previews bekommen bei Trailer-only-Tails denselben schwarzen Tail wie der
|
||
Export, damit der Browser nicht einen zu kurzen Source-Clip gegen den längeren
|
||
Referenzbeat weiterspult oder loopt.
|
||
Zur Synchronprüfung rendert der Report ein einzelnes Frame-Locked-Compare-Video
|
||
mit Referenz und Source in demselben MP4-Stream. Dieses Compare-Video ist
|
||
maßgeblich, weil zwei getrennte Browser-Videoelemente nie zuverlässig
|
||
framegenau synchron bleiben.
|
||
|
||
Wenn ein Trailer-Beat am Ende eine Blende, Schwarzfläche oder Textkarte enthält,
|
||
die im Source-Film nicht als normaler Shot vorhanden ist, endet der Source-Match
|
||
am letzten stabil passenden Frame. Exportierte Timelines behalten trotzdem die
|
||
volle Beat-Länge und fügen danach automatisch einen schwarzen Trailer-Tail mit
|
||
Marker für Fade/Dissolve ein.
|
||
|
||
Gezielte Ein-Beat-Matches nutzen zusätzlich vorhandene automatische Nachbarbeats
|
||
aus dem Cache als zeitliche Suchanker. Das hilft bei aufeinanderfolgenden Shots,
|
||
ohne manuelle Szenen oder Timecodes zu kuratieren.
|
||
Bei `match --beat N` wird ein alter Cache-Treffer für genau diesen Beat entfernt
|
||
und nur ein neu gefundener automatischer Treffer wieder eingetragen. Ein
|
||
fehlgeschlagener neuer Lauf kann dadurch keinen alten falschen Report-Treffer
|
||
stehen lassen.
|
||
|
||
Der globale Bildvergleich arbeitet auf kontrast-normalisierten Luma- und
|
||
Kantenfeatures statt auf rohen Farb-Pixeln. Dadurch bleiben Schwarzweiß- oder
|
||
anders gegradete Trailerbilder mit dem Source-Material vergleichbar, während
|
||
unähnliche Farbshots schlechter ranken.
|
||
Die Inpoint-Feinjustage bestimmt den Versatz lokal aus dem Bildinhalt: Um einen
|
||
groben Treffer herum werden mehrere Referenzframes gegen mehrere Source-Offsets
|
||
verglichen, und der beste gemeinsame Offset wird übernommen. Das ist schneller
|
||
als ein erneuter globaler Scan und vermeidet pauschale Frame-Prerolls.
|
||
Zusätzlich wird die Bewegungsphase über Frame-zu-Frame-Differenzen verglichen.
|
||
Dadurch kann der Matcher innerhalb derselben Source-Szene unterscheiden, ob
|
||
zwei Figuren noch sprechen, sich annähern, bereits im Kontakt sind oder sich
|
||
wieder voneinander lösen. Ein optisch ähnlicher Standbild-Treffer reicht damit
|
||
nicht mehr aus, wenn der Bewegungsverlauf nicht zur Referenz passt.
|
||
Schwarze Referenzframes aus Blenden oder Titel-Tails werden für diese
|
||
Offset-Messung ausgelassen, damit echte Bildbewegung und nicht die Blende selbst
|
||
den Inpoint bestimmt.
|
||
`rematch --refine` nutzt denselben lokalen FFmpeg/Pillow-Aligner und schreibt
|
||
den korrigierten Inpoint direkt zurück in `.cache/match_results.json`.
|
||
|
||
Zusätzlich werden aus den besten szenenweiten Luma/Histogramm-Kandidaten
|
||
mehrere Inpoint-Suchanker erzeugt. Diese Scene-Seeds verwenden keine harte
|
||
pHash-Sperre, weil pHash bei stark anders gegradeten Trailerbildern echte
|
||
Matches zu früh ausschließen kann.
|
||
Optional kann `python cli.py match --beat N --vision` einen Vision-Layer
|
||
zuschalten. Dann werden pro Trailer-Beat und pro wenigen Scene-Level-Kandidaten
|
||
je drei Frames (Anfang, Mitte, Ende) von einem visionfähigen OpenAI-kompatiblen
|
||
Modell beschrieben. Die Beschreibungen liegen in
|
||
`.cache/vision_descriptions.json` und werden wiederverwendet. Vision erzeugt
|
||
nur zusätzliche Suchanker; der eigentliche Match muss weiterhin durch CV,
|
||
Content-Reranking, Timing und Duration-Coverage bestätigt werden.
|
||
Gecachte Szenenbeschreibungen zählen nur, wenn sie vom aktuell konfigurierten
|
||
Vision-Modell stammen. Bei langen semantisch passenden Source-Szenen beschreibt
|
||
der Vision-Layer zusätzlich wenige lokale Zeitfenster und cached auch diese
|
||
Fenster, damit eine grob ähnliche Szene nicht automatisch mit dem falschen
|
||
Bewegungs- oder Dialogmoment gleichgesetzt wird.
|
||
Dieser lokale Fenster-Probe ist bewusst breiter als die finale Seed-Auswahl:
|
||
Eine lange Dialogszene kann in der Gesamtbeschreibung nur als Gespräch
|
||
erscheinen, aber an einer späteren Stelle trotzdem genau die gesuchte
|
||
Aktionsphase enthalten.
|
||
Für diese Probe wird deshalb die grobe Szenenähnlichkeit ohne harte
|
||
Aktionsstrafe gerankt; die harte Aktionsprüfung greift erst auf den lokalen
|
||
Fenstern und dem finalen Source-Zeitbereich.
|
||
Nach dem CV-Match kann derselbe Vision-Layer den konkreten finalen Source-
|
||
Zeitbereich nochmals gegen den Trailer-Beat prüfen. Starke Aktionsphasen wie
|
||
Annäherung, Kuss/Stirnkontakt, Handbewegungen oder Schneiden müssen dann auch
|
||
im Source-Fenster beschrieben sein; fehlt diese Aktionsphase, wird der Treffer
|
||
nicht gespeichert, selbst wenn der Low-Level-CV-Score hoch ist.
|
||
Wenn die Szene selbst plausibel ist, aber der konkrete Source-Zeitpunkt diese
|
||
Aktionsphase verfehlt, sucht der Matcher automatisch dichter innerhalb derselben
|
||
Source-Szene nach lokalen Vision-Fenstern mit der passenden Aktion und richtet
|
||
den Inpoint mit der Motion-Phase-Prüfung darauf neu aus. Erst wenn auch diese
|
||
In-Scene-Reparatur scheitert, wird der Treffer verworfen.
|
||
Diese In-Scene-Reparatur läuft auch für semantisch gültige Treffer aus langen
|
||
Source-Szenen. Dadurch kann ein grob passender Dialogmoment nicht bestehen
|
||
bleiben, wenn ein anderes lokales Fenster derselben Szene die gesuchte
|
||
Aktionsphase und Bewegung klarer trifft.
|
||
Die Kandidatenbewertung dieser Reparatur vergleicht dabei zwei Kontexte:
|
||
den ganzen Beat als semantischen Handlungsrahmen und das konkret sichtbare
|
||
Beat-Segment als Phasenprüfung. Ein Source-Zeitpunkt muss also nicht nur
|
||
"die Szene mit dem Kuss" enthalten, sondern auch zur aktuellen Bewegungsphase
|
||
des sichtbaren Trailerabschnitts passen. Pro Kandidat fließt zusätzlich ein
|
||
lokaler Content-/Motion-Frame-Score ein, damit cached Vision-Beschreibungen
|
||
keinen sichtbar versetzten Bewegungsmoment überstimmen.
|
||
Bei blendigen oder segmentierten Beats nutzt die semantische Action-Suche den
|
||
ganzen Trailerbeat als Kontext. Die eigentliche Frame-Ausrichtung bleibt auf das
|
||
sichtbare Segment begrenzt; der gefundene Source-Inpoint wird dabei um den
|
||
Trailer-Offset des Segments verschoben. So geht die globale Aktionsbeschreibung
|
||
eines Beats nicht verloren, nur weil der scorebare Teil erst nach einer Blende
|
||
beginnt.
|
||
Die Suche nach diesem Action-Window prüft pro Segment zwei Beschreibungen:
|
||
zuerst die des konkret sichtbaren Segments (so trifft die Phasensuche genau die
|
||
gerade gezeigte Bewegung), als Rückfall die des gesamten Beats. Der Beat-
|
||
Kontext gewinnt nur, wenn er deutlich (>0.06) besser scort; sonst bleibt das
|
||
Segment-Fenster die Wahl, weil die Beat-Beschreibung Aktionen aus Fade-Bildern
|
||
mit aufnehmen kann, die im sichtbaren Segment nicht stattfinden.
|
||
Der Trailer-Offset-Shift wird nur angewendet, wenn tatsächlich der Beat-Kontext
|
||
benutzt wurde; bei segmentbasierter Wahl ist das gefundene Fenster bereits auf
|
||
die sichtbare Aktionsphase ausgerichtet.
|
||
Der Segment-Offset zählt dabei nur über vorherige scorebare Bildinseln, nicht
|
||
über schwarze oder blendige Lücken. Nach dem Retiming wird die nutzbare
|
||
Source-Dauer erneut geschätzt; läuft die Source am Ende in eine sichtbar andere
|
||
Aktionsphase, wird der Clip gekürzt und der Rest bleibt Placeholder/Fade statt
|
||
einen falschen Bewegungsmoment zu zeigen.
|
||
Der gewichtete Vision-Seed-Pfad ersetzt standardmäßig keinen normalen
|
||
FFmpeg-Vollscan. Vision-Beschreibungen sind semantische Hinweise, aber keine
|
||
Beweise; der volle CV-Scan bleibt deshalb aktiv, damit falsch bewertete
|
||
Vision-Szenen echte Treffer nicht verdrängen. Für schnelle Experimente kann
|
||
`skip_coarse_scan_with_weighted_seeds = true` gesetzt werden.
|
||
Gewichtete Vision-Seeds werden nicht zuerst durch den alten Midpoint-Template
|
||
Refine verschoben; sie gehen direkt in die lokale Content-Alignment-Prüfung.
|
||
Das schützt wiederholte Gesprächseinstellungen, bei denen ähnliche Momente
|
||
mehrfach in derselben Szene vorkommen.
|
||
Innerhalb der automatisch von Vision vorgeschlagenen Szenen läuft zusätzlich
|
||
eine dichte lokale Bildsequenzsuche. Sie misst den Phasenversatz in kleinen
|
||
Zeitschritten direkt am Bildinhalt und bevorzugt Kandidaten mit genügend
|
||
Restdauer in derselben Source-Szene. Das ist kein manueller Override: Vision
|
||
grenzt nur Suchbereiche ein, die Auswahl bleibt Content-, Timing- und
|
||
Coverage-getrieben.
|
||
Nach einem dichten Vision-Treffer darf der spätere lokale Aligner nur noch im
|
||
Bereich dieses Scan-Schritts nachjustieren. So kann ein korrekt gefundener
|
||
Bewegungsmoment nicht wieder um viele Frames in eine ähnlich aussehende Phase
|
||
derselben Szene verschoben werden.
|
||
Für Vision-Action-Fenster nutzt die finale Retiming-Prüfung eine gemeinsame
|
||
Content-und-Motion-Suche pro Frame. Content und Bewegungsphase werden dabei
|
||
nicht mehr als zwei getrennte Korrekturschritte angewendet; das verhindert,
|
||
dass eine kurze Geste erst korrekt erkannt und anschließend in eine spätere
|
||
ähnliche Körperhaltung verschoben wird.
|
||
Wenn mehrere Vision-Kandidaten in derselben Source-Szene ähnlich gut scoren
|
||
und die Beat-Dauer abdecken, bevorzugt der Matcher die frühere Phase. Das
|
||
verhindert, dass ein späterer, minimal stärkerer Standbildtreffer die
|
||
Bewegungsphase des Trailers sichtbar überholt.
|
||
Enthält ein Trailerbeat selbst einen harten Umschnitt, werden Kandidaten an
|
||
angrenzenden Source-Szenengrenzen zusätzlich als zusammenhängender Multi-Shot-
|
||
Span geprüft. Ein Match darf dann über eine Source-Szenengrenze laufen, aber
|
||
nur wenn die relative Source-Grenze zeitlich zu einem erkannten Trailer-Umschnitt
|
||
passt. So kann ein Beat aus Frage/Antwort-Shots vollständig erfasst werden,
|
||
ohne Szenen willkürlich zusammenzukleben.
|
||
Auch der lokale Content-Aligner darf einen Inpoint nur noch übernehmen, wenn
|
||
die feste Whole-Frame-/Spatial-Validation dadurch besser wird.
|
||
Vor dem teuren Frame-Refine wird der gesamte Kandidatenpool mit einer schnellen
|
||
festen Inhaltsprüfung neu sortiert. Dadurch können korrekte Treffer aus
|
||
wiederholten Einstellungen einer Szene nach oben kommen, auch wenn ein freier
|
||
Template-Peak an anderer Stelle numerisch stärker war. Suchanker bleiben im
|
||
Pool erhalten, dürfen aber erst nach der Inhaltsprüfung nach oben rücken. Wenn
|
||
ein Kandidat visuell plausibel ist, aber wegen Trailerblende oder kurzem
|
||
Source-Span die normale Coverage knapp verfehlt, wird er als provisional Match
|
||
behalten statt als `NO MATCH` verworfen.
|
||
Dieses Reranking berücksichtigt zusätzlich die verbleibende Szenenlänge ab dem
|
||
Kandidaten-Inpoint. Dadurch werden zu späte ähnliche Gesprächsphasen innerhalb
|
||
derselben Szene nicht mehr vor frühere, tragfähigere Phasen sortiert.
|
||
Das Inhalts-Reranking nutzt bewusst nur wenige repräsentative Referenzframes und
|
||
eine begrenzte Kandidatenzahl. So bleiben wiederholte Szenen auffindbar, ohne
|
||
dass der Lauf durch tausende Random-Seeks minutenlang festhängt.
|
||
Confirmed Matches werden zusätzlich durch eine feste nahezu-Whole-Frame-Prüfung
|
||
aus Luma, Kanten, Farbhistogramm und räumlichen 4x4-Farbhistogrammen gedeckelt.
|
||
Dadurch kann ein freier Template-Hit mit ähnlicher Fenster-/Gesichtsstruktur
|
||
nicht mehr als sicherer Match gelten, wenn die Gesamtkomposition oder die
|
||
Bewegungsphase sichtbar eine andere Szene ist.
|
||
Für gewichtete Vision-Kandidaten gibt es zusätzlich eine eigene Provisional-
|
||
Bewertung aus Content-Score, Restdauer und Seed-Stärke. Dadurch können echte,
|
||
aber durch Trailer-Grading/Crop numerisch schwache Treffer im Report landen,
|
||
ohne als confirmed Match durchzugehen.
|
||
Die Cache-Normalisierung für Report/Export verwendet dieselbe niedrigere
|
||
Content-Untergrenze für nicht bestätigte Vision-Provisional-Treffer, damit ein
|
||
gerade gefundener automatischer Match nicht beim Report-Aufbau wieder
|
||
weggefiltert wird.
|
||
Sie übernimmt auch die Multi-Shot-Coverage-Regel: gecachte Treffer, die passend
|
||
zu internen Trailer-Umschnitten über angrenzende Source-Szenen laufen, werden
|
||
nicht mehr auf die erste Source-Szene zurückgekürzt.
|
||
Gezielte Einzel-Beat-Matches gewichten außerdem die automatisch aus Nachbarbeats
|
||
abgeleiteten Continuity-Seeds. Wenn ein Beat direkt an einen bereits passenden
|
||
Vorgänger anschließt, kann ein späterer ähnlich aussehender Moment derselben
|
||
Dialogszene den erwarteten Anschluss nicht mehr nur wegen eines höheren
|
||
Standbildscores verdrängen.
|
||
Diese Continuity-Seeds sind aber nur Suchanker: in derselben Szene darf ein
|
||
späterer Inpoint gewinnen, wenn die mehrframeige Content-Prüfung die
|
||
Bewegungsphase klar besser trifft. Dadurch bleiben Anschlussmatches stabil,
|
||
ohne Hand-/Kopfbewegungen auf einen falschen Zeitpunkt festzunageln.
|
||
Continuity- und Vision-Seeds allein schalten den globalen FFmpeg-Scan
|
||
standardmäßig nicht ab. Sie sind Suchanker, keine Beweise; der volle CV-Scan
|
||
bleibt aktiv, damit semantisch plausible, aber falsche Vision-Treffer echte
|
||
Bildmatches nicht verdrängen.
|
||
Bei aktivierter Vision wird für gezielte Match-Läufe trotzdem zuerst ein
|
||
schneller seed-basierter CV-Prepass ausgeführt. Er überspringt den vollen
|
||
FFmpeg-Stream nur vorläufig und akzeptiert einen Treffer erst nach derselben
|
||
Bild-/Phasenvalidierung wie der normale Matcher. Nur nicht gelöste Beats fallen
|
||
danach auf den vollständigen Scan zurück. Die Qualitätsparameter für lokale
|
||
Vision-Szenenscans und Refine-Kandidaten bleiben dabei erhalten; der Prepass ist
|
||
eine Reihenfolge-Optimierung, kein Qualitätsdeckel.
|
||
Provisional Treffer aus diesem schnellen Prepass sind nicht endgültig: wenn sie
|
||
unterhalb der Confirmed-Schwelle bleiben, läuft zusätzlich der vollständige
|
||
CV-Scan und darf den besseren oder bestätigten Treffer übernehmen.
|
||
OpenRouter-/Vision-Rate-Limits werden mit progressiv längeren Pausen erneut
|
||
versucht. Billing-, Credit- oder Token-Guthaben-Fehler werden dagegen sofort als
|
||
echter Blocker gemeldet, weil Warten dort nicht hilft.
|
||
Auch Netzfehler beim Lesen der Antwort (Timeouts, Verbindungsabbrüche während
|
||
einer DSL-Trennung) werden als retrybar behandelt, nicht nur Verbindungsfehler
|
||
beim Verbindungsaufbau. Schlägt die Vision-Verifikation während der finalen
|
||
Filter-/Repair-Stufe trotzdem dauerhaft fehl, wird der bisherige gecachte
|
||
Treffer für diesen Beat behalten statt verworfen — ein Netzproblem darf keinen
|
||
schon korrekt gefundenen Match aus dem Cache löschen.
|
||
Die Phasen-Reparatur an gefundenen Treffern läuft nicht mehr nur in „langen"
|
||
Source-Szenen, sondern überall dort, wo die Szene mehr als nur das
|
||
Segment-Fenster trägt. Eine korrigierte Position wird übernommen, sobald sie
|
||
das Bildinhalt-Validate besteht UND nicht spürbar schlechter scort als das
|
||
Original (≤ 0.02 Verlust). Bereits bestätigte Treffer in eng zugeschnittenen
|
||
Szenen werden bewusst nicht angefasst, damit ein guter Match nicht durch eine
|
||
nominell gleichwertige Alternative ausgetauscht wird.
|
||
Beats, die nach dem CV-Lauf weder als Vollmatch noch als Segmentmatch landen,
|
||
durchlaufen anschließend eine Recovery-Stufe: Vibe-Check (Histogramm/pHash)
|
||
liefert Top-K Kandidatenszenen, die semantische Action-Window-Suche prüft
|
||
darin die Phase des sichtbaren Trailerbeat-Anteils, und der CV-Aligner setzt
|
||
den Inpoint frame-genau. Übernommen wird nur ein Kandidat, der dieselbe
|
||
Vision-Phasenvalidierung wie der Hauptpfad besteht. Beats ohne sichtbares
|
||
Bildmaterial (Logos, Titel-Karten, durchgehende Fades) werden gar nicht erst
|
||
gesucht — sie sind bewusst kein Match.
|
||
Lange Trailerbeats werden nicht mehr automatisch über ihre gesamte Beat-Länge
|
||
gegen einen einzigen Source-Clip validiert. Sobald nach einem sichtbaren
|
||
Source-Abschnitt eine anhaltende Schwarzblende oder Titel-/Credit-Insel beginnt,
|
||
endet der matchbare Referenzbereich dort; zwei aufeinanderfolgende dunkle
|
||
Samples reichen dafür. Spätere Text-/Creditbilder im selben Beat gehen damit
|
||
nicht mehr in Reranking, Validation oder Span-Schätzung ein.
|
||
Wenn nach einer Blende wieder sichtbares, matchbares Material kommt, wird der
|
||
Beat nicht mehr als „Source + schwarzer Tail“ behandelt. Der CLI-Match speichert
|
||
zusätzliche `MatchSegment`-Einträge für jede automatisch erkannte sichtbare
|
||
Insel; der HTML-Report setzt diese Source-Segmente frame-lockend zusammen und
|
||
füllt nur echte Zwischenlücken mit Schwarz. Dadurch können per Blende verbundene
|
||
Trailer-Einstellungen innerhalb eines Beats getrennt gematcht werden, ohne die
|
||
globale Scene Detection aggressiver oder beat-spezifisch zu kuratieren.
|
||
Beats mit mehreren sichtbaren Inseln werden direkt segmentiert gesucht, statt
|
||
zuerst als ein künstlich zusammenhängender Source-Clip über den ganzen Film zu
|
||
laufen. Jede Insel nutzt dieselbe gestufte Vision-/CV-Validierung wie ein
|
||
normaler Beat; der zusammengesetzte Report bleibt beat-synchron. Wenn der
|
||
schnelle validierte Vision-Prepass für eine Insel keinen Treffer liefert, darf
|
||
diese Insel weiterhin in den vollständigen Scan fallen.
|
||
Falls ein kompletter Beat keinen belastbaren Einzelclip ergibt, versucht der
|
||
Matcher dieselbe Segmentlogik automatisch als Fallback: sichtbare Inseln werden
|
||
einzeln global gesucht und anschließend wieder zu einem Beat-Ergebnis
|
||
zusammengesetzt. Sehr kurze Inseln dürfen zusätzlich in den Source-Szenen
|
||
benachbarter bereits gematchter Beats lokal nach ihrer Bewegungsphase suchen.
|
||
Das ist weiterhin nur ein allgemeiner Continuity-Anker, kein manueller Override
|
||
für bestimmte Beat-Nummern oder Szenen.
|
||
Besteht ein Beat nach automatischer Fade-/Titel-Filterung nur aus einer
|
||
einzigen sichtbaren Insel, wird diese Insel direkt als primäres Suchziel
|
||
verwendet. Dadurch scannt der Matcher denselben Bildinhalt nicht erst als
|
||
vollen Beat und danach noch einmal als Segment; der Report behält trotzdem die
|
||
korrekte Beat-Position und füllt echte Randlücken mit Schwarz.
|
||
Gecachte segmentierte Treffer werden ebenfalls gegen die automatisch sichtbare
|
||
Referenzdauer normalisiert, nicht gegen Schwarz-/Blendränder des gesamten Beats.
|
||
Ein korrekt gematchter kurzer Bildinhalt wird dadurch beim Report-Aufbau nicht
|
||
nachträglich als zu kurz verworfen.
|
||
Zusätzlich werden sehr dunkle, kontrastarme oder noch nicht sauber
|
||
auf-/abgeblendete Referenzframes aus Score, Inhalts-Reranking,
|
||
Phasen-Alignment und Motion-Templates herausgenommen. Blenden sollen bestimmen,
|
||
wie der Clip später exportiert wird, aber nicht, ob der Bildinhalt als Match
|
||
gilt.
|
||
Sichtbare Fade-Rampen werden nur in eine matchbare Insel hinein erweitert, wenn
|
||
sie strukturell stark zum ersten bzw. letzten scorebaren Frame derselben
|
||
Einstellung passen. Doppelbelichtungen aus Cross-Dissolves bleiben dadurch
|
||
Übergangsmaterial und werden nicht als einzelner Quellclip erzwungen.
|
||
Treffer unter `provisional_content_threshold` werden gar nicht mehr gespeichert
|
||
oder aus alten Cache-Ergebnissen übernommen. Das verhindert, dass offensichtlich
|
||
falsche Szenen im Report als Match-Kandidat weiterleben.
|
||
|
||
### 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.
|