Add vision prepass for targeted matches
This commit is contained in:
@@ -228,6 +228,11 @@ Continuity- und Vision-Seeds allein schalten den globalen FFmpeg-Scan
|
|||||||
standardmäßig nicht ab. Sie sind Suchanker, keine Beweise; der volle CV-Scan
|
standardmäßig nicht ab. Sie sind Suchanker, keine Beweise; der volle CV-Scan
|
||||||
bleibt aktiv, damit semantisch plausible, aber falsche Vision-Treffer echte
|
bleibt aktiv, damit semantisch plausible, aber falsche Vision-Treffer echte
|
||||||
Bildmatches nicht verdrängen.
|
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.
|
||||||
Lange Trailerbeats werden nicht mehr automatisch über ihre gesamte Beat-Länge
|
Lange Trailerbeats werden nicht mehr automatisch über ihre gesamte Beat-Länge
|
||||||
gegen einen einzigen Source-Clip validiert. Sobald nach einem sichtbaren
|
gegen einen einzigen Source-Clip validiert. Sobald nach einem sichtbaren
|
||||||
Source-Abschnitt eine anhaltende Schwarzblende oder Titel-/Credit-Insel beginnt,
|
Source-Abschnitt eine anhaltende Schwarzblende oder Titel-/Credit-Insel beginnt,
|
||||||
@@ -248,6 +253,11 @@ zusammengesetzt. Sehr kurze Inseln dürfen zusätzlich in den Source-Szenen
|
|||||||
benachbarter bereits gematchter Beats lokal nach ihrer Bewegungsphase suchen.
|
benachbarter bereits gematchter Beats lokal nach ihrer Bewegungsphase suchen.
|
||||||
Das ist weiterhin nur ein allgemeiner Continuity-Anker, kein manueller Override
|
Das ist weiterhin nur ein allgemeiner Continuity-Anker, kein manueller Override
|
||||||
für bestimmte Beat-Nummern oder Szenen.
|
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.
|
||||||
Zusätzlich werden sehr dunkle, kontrastarme oder noch nicht sauber
|
Zusätzlich werden sehr dunkle, kontrastarme oder noch nicht sauber
|
||||||
auf-/abgeblendete Referenzframes aus Score, Inhalts-Reranking,
|
auf-/abgeblendete Referenzframes aus Score, Inhalts-Reranking,
|
||||||
Phasen-Alignment und Motion-Templates herausgenommen. Blenden sollen bestimmen,
|
Phasen-Alignment und Motion-Templates herausgenommen. Blenden sollen bestimmen,
|
||||||
|
|||||||
@@ -485,6 +485,71 @@ def _reference_scoreable_segments(beat, cfg) -> list[tuple[float, float]]:
|
|||||||
return raw
|
return raw
|
||||||
|
|
||||||
|
|
||||||
|
def _trim_beats_to_single_visual_island(beats: list, cfg) -> tuple[list, dict[int, tuple[float, float]]]:
|
||||||
|
"""Use a single visible island as the primary match target for faded beats."""
|
||||||
|
from dataclasses import replace
|
||||||
|
|
||||||
|
trimmed = []
|
||||||
|
trims: dict[int, tuple[float, float]] = {}
|
||||||
|
frame_s = 1.0 / max(1.0, float(cfg.export.edl_frame_rate))
|
||||||
|
for beat in beats:
|
||||||
|
islands = _reference_scoreable_segments(beat, cfg)
|
||||||
|
if len(islands) == 1:
|
||||||
|
start_s, end_s = islands[0]
|
||||||
|
island_duration_s = max(0.0, end_s - start_s)
|
||||||
|
has_real_trim = (
|
||||||
|
start_s > frame_s * 1.5
|
||||||
|
or beat.duration_s - end_s > frame_s * 1.5
|
||||||
|
)
|
||||||
|
if island_duration_s > 0.0 and has_real_trim:
|
||||||
|
trimmed.append(
|
||||||
|
replace(
|
||||||
|
beat,
|
||||||
|
start_s=beat.start_s + start_s,
|
||||||
|
end_s=beat.start_s + end_s,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
trims[beat.beat_id] = (start_s, island_duration_s)
|
||||||
|
continue
|
||||||
|
trimmed.append(beat)
|
||||||
|
return trimmed, trims
|
||||||
|
|
||||||
|
|
||||||
|
def _apply_single_island_segments(results: list, trims: dict[int, tuple[float, float]]) -> list:
|
||||||
|
"""Restore beat-relative segment metadata after matching a trimmed island."""
|
||||||
|
if not trims:
|
||||||
|
return results
|
||||||
|
|
||||||
|
from dataclasses import replace
|
||||||
|
from src.core.models import MatchSegment
|
||||||
|
|
||||||
|
expanded = []
|
||||||
|
for result in results:
|
||||||
|
trim = trims.get(result.beat_id)
|
||||||
|
if trim is None or getattr(result, "segments", ()):
|
||||||
|
expanded.append(result)
|
||||||
|
continue
|
||||||
|
trailer_offset_s, island_duration_s = trim
|
||||||
|
duration_s = min(max(0.0, island_duration_s), max(0.0, result.duration_s))
|
||||||
|
segment = MatchSegment(
|
||||||
|
trailer_offset_s=trailer_offset_s,
|
||||||
|
duration_s=duration_s,
|
||||||
|
scene_id=result.scene_id,
|
||||||
|
in_point_s=result.in_point_s,
|
||||||
|
out_point_s=result.in_point_s + duration_s,
|
||||||
|
match_score=result.match_score,
|
||||||
|
is_confirmed=result.is_confirmed,
|
||||||
|
)
|
||||||
|
expanded.append(
|
||||||
|
replace(
|
||||||
|
result,
|
||||||
|
out_point_s=result.in_point_s + duration_s,
|
||||||
|
segments=(segment,),
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return expanded
|
||||||
|
|
||||||
|
|
||||||
def _attach_visual_segments(results: list, beats: list, cfg) -> list:
|
def _attach_visual_segments(results: list, beats: list, cfg) -> list:
|
||||||
"""Attach automatic sub-shot matches for multi-island trailer beats."""
|
"""Attach automatic sub-shot matches for multi-island trailer beats."""
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
@@ -558,7 +623,13 @@ def _attach_visual_segments(results: list, beats: list, cfg) -> list:
|
|||||||
return expanded
|
return expanded
|
||||||
|
|
||||||
|
|
||||||
def _match_unmatched_visual_segments(results: list, beats: list, cached: list, cfg) -> list:
|
def _match_unmatched_visual_segments(
|
||||||
|
results: list,
|
||||||
|
beats: list,
|
||||||
|
cached: list,
|
||||||
|
cfg,
|
||||||
|
skip_global_segment_scan_for: set[int] | None = None,
|
||||||
|
) -> list:
|
||||||
"""Create segmented provisional matches when a whole beat has no single match."""
|
"""Create segmented provisional matches when a whole beat has no single match."""
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
from src.core.models import MatchResult, MatchSegment
|
from src.core.models import MatchResult, MatchSegment
|
||||||
@@ -567,6 +638,7 @@ def _match_unmatched_visual_segments(results: list, beats: list, cached: list, c
|
|||||||
|
|
||||||
matched_ids = {r.beat_id for r in results}
|
matched_ids = {r.beat_id for r in results}
|
||||||
expanded = list(results)
|
expanded = list(results)
|
||||||
|
skip_global_segment_scan_for = skip_global_segment_scan_for or set()
|
||||||
try:
|
try:
|
||||||
fps = float(get_video_info(cfg.paths.source_movie)["fps"]) or cfg.export.edl_frame_rate
|
fps = float(get_video_info(cfg.paths.source_movie)["fps"]) or cfg.export.edl_frame_rate
|
||||||
except Exception:
|
except Exception:
|
||||||
@@ -593,11 +665,13 @@ def _match_unmatched_visual_segments(results: list, beats: list, cached: list, c
|
|||||||
cached + expanded,
|
cached + expanded,
|
||||||
cfg,
|
cfg,
|
||||||
)
|
)
|
||||||
segment_matches = run_global_scan(
|
segment_matches = []
|
||||||
[segment_beat],
|
if beat.beat_id not in skip_global_segment_scan_for:
|
||||||
cfg,
|
segment_matches = run_global_scan(
|
||||||
seed_in_points=continuity,
|
[segment_beat],
|
||||||
)
|
cfg,
|
||||||
|
seed_in_points=continuity,
|
||||||
|
)
|
||||||
if not segment_matches:
|
if not segment_matches:
|
||||||
local_segment = _local_same_scene_segment_match(
|
local_segment = _local_same_scene_segment_match(
|
||||||
segment_beat,
|
segment_beat,
|
||||||
@@ -725,18 +799,48 @@ def cmd_match(args: argparse.Namespace, cfg) -> list:
|
|||||||
all_beats = _load_beats(cfg)
|
all_beats = _load_beats(cfg)
|
||||||
beats = _select_beats(all_beats, getattr(args, "beat", None))
|
beats = _select_beats(all_beats, getattr(args, "beat", None))
|
||||||
cached = _normalize_cached_results(all_beats, _load_results(cfg), cfg) if _results_cache_path(cfg).exists() else []
|
cached = _normalize_cached_results(all_beats, _load_results(cfg), cfg) if _results_cache_path(cfg).exists() else []
|
||||||
|
scan_beats, single_island_trims = _trim_beats_to_single_visual_island(beats, cfg)
|
||||||
seed_in_points = (
|
seed_in_points = (
|
||||||
_continuity_seed_in_points(args.beat, all_beats, cached, cfg)
|
_continuity_seed_in_points(args.beat, all_beats, cached, cfg)
|
||||||
if getattr(args, "beat", None) is not None
|
if getattr(args, "beat", None) is not None
|
||||||
else None
|
else None
|
||||||
)
|
)
|
||||||
results = run_matching(
|
results = []
|
||||||
cfg,
|
if cfg.vision.enabled:
|
||||||
|
fast_cfg = replace(
|
||||||
|
cfg,
|
||||||
|
cv=replace(
|
||||||
|
cfg.cv,
|
||||||
|
deep_scan=replace(cfg.cv.deep_scan, skip_coarse_scan_with_weighted_seeds=True),
|
||||||
|
),
|
||||||
|
vision=replace(cfg.vision, fullscan_fallback=False),
|
||||||
|
)
|
||||||
|
results = run_matching(
|
||||||
|
fast_cfg,
|
||||||
|
scan_beats,
|
||||||
|
force_reindex=args.force_reindex,
|
||||||
|
seed_in_points=seed_in_points,
|
||||||
|
)
|
||||||
|
|
||||||
|
if len(results) < len(scan_beats):
|
||||||
|
matched_ids = {r.beat_id for r in results}
|
||||||
|
remaining_beats = [b for b in scan_beats if b.beat_id not in matched_ids]
|
||||||
|
if remaining_beats:
|
||||||
|
full_results = run_matching(
|
||||||
|
cfg,
|
||||||
|
remaining_beats,
|
||||||
|
force_reindex=args.force_reindex,
|
||||||
|
seed_in_points=seed_in_points,
|
||||||
|
)
|
||||||
|
results = sorted([*results, *full_results], key=lambda r: r.beat_id)
|
||||||
|
results = _apply_single_island_segments(results, single_island_trims)
|
||||||
|
results = _match_unmatched_visual_segments(
|
||||||
|
results,
|
||||||
beats,
|
beats,
|
||||||
force_reindex=args.force_reindex,
|
cached,
|
||||||
seed_in_points=seed_in_points,
|
cfg,
|
||||||
|
skip_global_segment_scan_for=set(single_island_trims),
|
||||||
)
|
)
|
||||||
results = _match_unmatched_visual_segments(results, beats, cached, cfg)
|
|
||||||
results = _attach_visual_segments(results, beats, cfg)
|
results = _attach_visual_segments(results, beats, cfg)
|
||||||
|
|
||||||
# A targeted one-beat match should improve the cache without deleting
|
# A targeted one-beat match should improve the cache without deleting
|
||||||
|
|||||||
Reference in New Issue
Block a user