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>
299 lines
16 KiB
Markdown
299 lines
16 KiB
Markdown
# Algorithmus-Notizen
|
||
|
||
Detaillierte Verhaltensbeschreibung und Designentscheidungen des Matchers.
|
||
Für die normale Bedienung reicht die [README](../README.md) — dieses Dokument
|
||
ist die Referenz für den Tool-Verantwortlichen, wenn etwas im Verhalten
|
||
unklar wird oder ein Match systematisch falsch landet.
|
||
|
||
## HTML-Report und Vorschauclips
|
||
|
||
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.
|
||
|
||
## Trailer-Tails ohne Source-Pendant
|
||
|
||
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.
|
||
|
||
## Targeted single-beat re-matches
|
||
|
||
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.
|
||
|
||
## Bildvergleich auf Luma + Kanten
|
||
|
||
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.
|
||
|
||
## Bewegungsphasen-Vergleich
|
||
|
||
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`.
|
||
|
||
## Vision-Layer (optional)
|
||
|
||
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 die grobe Szenenähnlichkeit ohne harte Aktionsstrafe
|
||
gerankt; die harte Aktionsprüfung greift erst auf den lokalen Fenstern und
|
||
dem finalen Source-Zeitbereich.
|
||
|
||
## Aktionsphase-Verifikation nach dem CV-Match
|
||
|
||
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.
|
||
|
||
Die Kandidatenbewertung dieser Reparatur vergleicht zwei Kontexte: den ganzen
|
||
Beat als semantischen Handlungsrahmen und das konkret sichtbare Beat-Segment
|
||
als Phasenprüfung. Ein Source-Zeitpunkt muss 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.
|
||
|
||
## Segmentierte Beats (Beats mit Blenden / Inseln)
|
||
|
||
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 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 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.
|
||
|
||
## Vision-Seeds vs. Vollscan
|
||
|
||
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 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 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.
|
||
|
||
## Multi-Shot-Beats
|
||
|
||
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.
|
||
|
||
## Reranking-Pipeline
|
||
|
||
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, nutzt bewusst nur wenige repräsentative Referenzframes
|
||
und eine begrenzte Kandidatenzahl. Confirmed Matches werden zusätzlich durch
|
||
eine feste nahezu-Whole-Frame-Prüfung aus Luma, Kanten, Farbhistogramm und
|
||
räumlichen 4×4-Farbhistogrammen gedeckelt. Auch der lokale Content-Aligner
|
||
darf einen Inpoint nur noch übernehmen, wenn die feste Whole-Frame-/Spatial-
|
||
Validation dadurch besser wird.
|
||
|
||
Für gewichtete Vision-Kandidaten gibt es zusätzlich eine eigene Provisional-
|
||
Bewertung aus Content-Score, Restdauer und Seed-Stärke. Die Cache-
|
||
Normalisierung für Report/Export verwendet dieselbe niedrigere
|
||
Content-Untergrenze für nicht bestätigte Vision-Provisional-Treffer und
|
||
übernimmt 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.
|
||
|
||
## Continuity-Seeds
|
||
|
||
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.
|
||
|
||
## Vision-Prepass für gezielte Match-Läufe
|
||
|
||
Bei aktivierter Vision wird für gezielte Match-Läufe 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.
|
||
|
||
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.
|
||
|
||
## Fehlertoleranz bei Vision-API
|
||
|
||
OpenRouter-/Vision-Rate-Limits werden mit progressiv längeren Pausen erneut
|
||
versucht. Auch Netzfehler beim Lesen der Antwort (Timeouts,
|
||
Verbindungsabbrüche während einer DSL-Trennung) werden als retrybar
|
||
behandelt, nicht nur Verbindungsfehler beim Verbindungsaufbau.
|
||
Billing-, Credit- oder Token-Guthaben-Fehler werden dagegen sofort als
|
||
echter Blocker gemeldet, weil Warten dort nicht hilft.
|
||
|
||
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.
|
||
|
||
## Phasen-Reparatur und Recovery
|
||
|
||
Die Phasen-Reparatur an gefundenen Treffern läuft nicht 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.
|
||
|
||
## Behandlung von Blenden und Schwarzfeldern
|
||
|
||
Lange Trailerbeats werden nicht 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 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.
|
||
|
||
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. Falls ein kompletter Beat keinen belastbaren
|
||
Einzelclip ergibt, versucht der Matcher dieselbe Segmentlogik automatisch
|
||
als Fallback. Sehr kurze Inseln dürfen zusätzlich in den Source-Szenen
|
||
benachbarter bereits gematchter Beats lokal nach ihrer Bewegungsphase
|
||
suchen.
|
||
|
||
Besteht ein Beat nach automatischer Fade-/Titel-Filterung nur aus einer
|
||
einzigen sichtbaren Insel, wird diese Insel direkt als primäres Suchziel
|
||
verwendet. Gecachte segmentierte Treffer werden gegen die automatisch
|
||
sichtbare Referenzdauer normalisiert, nicht gegen Schwarz-/Blendränder
|
||
des gesamten Beats.
|
||
|
||
Sehr dunkle, kontrastarme oder noch nicht sauber auf-/abgeblendete
|
||
Referenzframes werden aus Score, Inhalts-Reranking, Phasen-Alignment und
|
||
Motion-Templates herausgenommen. Sichtbare Fade-Rampen werden nur in eine
|
||
matchbare Insel hinein erweitert, wenn sie strukturell stark zum ersten
|
||
bzw. letzten scorebaren Frame derselben Einstellung passen.
|
||
|
||
Treffer unter `provisional_content_threshold` werden nicht mehr gespeichert
|
||
oder aus alten Cache-Ergebnissen übernommen.
|