Match segment window by its own action phase

For segmented beats, the repair stage now searches for the source action
window using the segment's own description first; the full beat context is
used only as a fallback or when it scores noticeably higher. The trailer-
offset shift is applied only when the beat context is actually chosen.

Also harden vision-call retries to catch read-side network errors
(TimeoutError, socket.timeout, ConnectionError, OSError) and wrap the
filter/repair loop so a transient vision failure preserves the previously
cached match instead of dropping it.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Melbar
2026-05-03 06:22:12 +02:00
parent 2cc05e4737
commit 7b4a98d760
4 changed files with 183 additions and 5 deletions
+36 -5
View File
@@ -662,11 +662,26 @@ def _filter_semantically_invalid_vision_matches(results: list, beats: list, cfg)
scene = scenes_by_id.get(scene_id)
if scene is None:
return None
found = find_action_window_in_scene(action_beat or check_beat, scene, cfg)
segment_window = find_action_window_in_scene(check_beat, scene, cfg)
if action_beat is not None and action_beat is not check_beat:
beat_window = find_action_window_in_scene(action_beat, scene, cfg)
else:
beat_window = None
use_beat_context = False
if segment_window is None:
found = beat_window
use_beat_context = beat_window is not None
elif beat_window is None:
found = segment_window
elif beat_window[2] > segment_window[2] + 0.06:
found = beat_window
use_beat_context = True
else:
found = segment_window
if found is None:
return None
start_s, end_s, semantic_score, reason = found
if action_beat is not None:
if use_beat_context:
segment_start_offset_s = max(0.0, check_beat.start_s - action_beat.start_s)
content_offset_s = visible_content_offset(action_beat, segment_start_offset_s)
start_s += content_offset_s
@@ -713,6 +728,23 @@ def _filter_semantically_invalid_vision_matches(results: list, beats: list, cfg)
kept.append(result)
continue
kept_before = len(kept)
try:
_filter_repair_one(result, beat, beats_by_id, scenes_by_id, kept, cfg, realign_window, validate_match_window_with_vision, logger)
except Exception as exc:
logger.warning(
"Beat %d: vision filter/repair failed (%s); keeping previous cached match.",
result.beat_id,
exc,
)
del kept[kept_before:]
kept.append(result)
return kept
def _filter_repair_one(result, beat, beats_by_id, scenes_by_id, kept, cfg, realign_window, validate_match_window_with_vision, logger):
from dataclasses import replace
if True:
windows = []
if getattr(result, "segments", ()):
for segment in result.segments:
@@ -867,7 +899,7 @@ def _filter_semantically_invalid_vision_matches(results: list, beats: list, cfg)
is_confirmed=repaired_score >= cfg.cv.deep_scan.match_threshold,
segments=tuple(new_segments),
))
continue
return
else:
repair = realign_window(beat, result.scene_id)
if repair is not None:
@@ -886,13 +918,12 @@ def _filter_semantically_invalid_vision_matches(results: list, beats: list, cfg)
match_score=score,
is_confirmed=score >= cfg.cv.deep_scan.match_threshold,
))
continue
return
logger.warning(
"Beat %d: rejected by vision action-phase verification (%s)",
result.beat_id,
"; ".join(reasons),
)
return kept
def _attach_visual_segments(results: list, beats: list, cfg) -> list: