06a2326bf1
- Phase realign for matched results: drop the "long scene" gate (>1.6x segment, >=6s) in favor of "scene has any meaningful slack beyond the segment". Already-confirmed segments in tight scenes are still skipped to protect strong matches. A repair is only committed if the new score is not meaningfully worse than the original (>=score-0.02). - Recovery stage for unmatched beats: vibe-check (CV) feeds top-K candidate scenes into the semantic action-window search; CV alignment + vision phase validate gate decide whether the candidate becomes a provisional match. Beats without scoreable visual content (logos, title cards, full fades) remain unmatched by design. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
515 lines
28 KiB
Markdown
515 lines
28 KiB
Markdown
# AI Trailer Generator v2
|
||
|
||
**Frame-accurate trailer reconstruction via pure Computer Vision**
|
||
|
||
> Gibt einen Reference Trailer und den dazugehörigen Quellfilm hinein — bekommt eine fertige FCPXML/EDL heraus, die den Trailer Frame-genau aus dem Quellfilm nachbaut.
|
||
|
||
---
|
||
|
||
## Das Kernprinzip
|
||
|
||
Standardmäßig kein LLM für visuelles Matching. Optional kann ein Vision-Layer
|
||
gecachte 3-Frame-Beschreibungen als zusätzliche Suchanker liefern; der finale
|
||
Match bleibt aber CV-verifiziert.
|
||
|
||
| Phase | Was passiert | Technologie |
|
||
|-------|-------------|-------------|
|
||
| **0 — Prep** | Reference Trailer analysieren & Beats extrahieren | PySceneDetect + OpenCV |
|
||
| **1 — Global Scan**| Gesamten Quellfilm via FFmpeg-Stream (2 FPS) gegen alle Beats scannen | FFmpeg Pipe + Luma-Histogramm |
|
||
| **1b — Optional Vision Seeds** | Unsichere Top-K Szenen mit 3-Frame-Beschreibungen cachen | OpenAI-kompatibles Vision-LLM |
|
||
| **2 — Refine** | Beste Treffer auf Frame-Ebene präzisieren | OpenCV `matchTemplate` |
|
||
| **3 — Dramaturgie** | Narrative BeatType-Klassifikation aus Dialog-Text | OpenRouter LLM |
|
||
| **4 — Export** | Timeline → FCPXML 1.10 oder CMX 3600 EDL | xml.etree + eigener Timecode-Layer |
|
||
|
||
**Text-Safe Crop:** Obere 15% und untere 30% des Frames werden vor jedem Vergleich ausgeblendet, um Title Cards, Logos und Letterbox zu ignorieren.
|
||
|
||
---
|
||
|
||
## 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.
|