Fix matching regressions, cache guard, and multi-shot algorithm for beat 15

- config.toml: revert scoreable_luma/contrast thresholds to 24/58/24 (lowering
  them let cross-fade blend frames contaminate content-validation templates,
  dropping scores below provisional_content_threshold)
- src/cv/global_scan.py: _is_dark_reference_frame now requires contrast<30 so
  genuine dark silhouette frames are not rejected as scoreable; two-path
  _is_scoreable_reference_frame separates standard vs fade-content scoring
- cli.py: _keeps_cached_match() guard prevents a weaker single-span rematch
  from overwriting a better multi-segment provisional cache entry
- cli.py: _fade_content_shots() restricted to between-island gaps only—
  pre-island black leaders were incorrectly emitted as matchable shots
- cli.py: island[0] of _match_unmatched_visual_segments() now uses no
  continuity seed so an insert cut at the start of a multi-shot beat is not
  forced toward the previous beat's scene
- scripts/generate_cutter_report.py: fix ffmpeg concat demuxer on Windows—
  use part.absolute().as_posix() so paths in the concat txt are absolute and
  not double-resolved relative to the concat file's directory

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Melbar
2026-05-06 00:05:37 +02:00
parent 223789eafc
commit 54d3f04616
4 changed files with 186 additions and 26 deletions
+41 -6
View File
@@ -580,13 +580,24 @@ def _prepare_motion_templates(
def _is_dark_reference_frame(frame: np.ndarray, cfg: AppConfig) -> bool:
"""Truly dark / pure-black frame: no usable structure for matching.
A cross-fade silhouette (low overall luma but visible contrast) is NOT
a dark frame for our purposes — it carries content (a hand, a knife,
a face peeking through the fade) and should still be matchable.
"""
cropped = text_safe_crop(
frame,
cfg.cv.vibe_check.crop_top_fraction,
cfg.cv.vibe_check.crop_bottom_fraction,
)
gray = cv2.cvtColor(cropped, cv2.COLOR_BGR2GRAY)
return float(np.mean(gray)) < 28.0 and float(np.percentile(gray, 90)) < 58.0
mean = float(np.mean(gray))
p90 = float(np.percentile(gray, 90))
p10 = float(np.percentile(gray, 10))
contrast = p90 - p10
# Real darkness: low luma AND low contrast (no structure visible)
return mean < 28.0 and p90 < 58.0 and contrast < 30.0
def _reference_visibility_stats(frame: np.ndarray, cfg: AppConfig) -> tuple[float, float, float]:
@@ -602,16 +613,40 @@ def _reference_visibility_stats(frame: np.ndarray, cfg: AppConfig) -> tuple[floa
def _is_scoreable_reference_frame(frame: np.ndarray, cfg: AppConfig) -> bool:
"""Exclude black, fade, and low-visibility reference frames from scoring."""
"""Decide whether a reference frame can carry a usable match template.
Two acceptance paths:
* Standard: regular daylight / interior shot — luma at or above the
configured thresholds AND enough contrast to be distinct.
* Fade-content: low overall luma BUT with strong local contrast,
i.e. a cross-fade silhouette where you can clearly see structure
(hand+knife against dark, face emerging from black, etc.). Without
this path the matcher would silently drop content-bearing fades and
mis-match the visible portion alone.
"""
if _is_dark_reference_frame(frame, cfg):
return False
mean_luma, p90_luma, contrast = _reference_visibility_stats(frame, cfg)
low_visibility = (
mean_luma < cfg.cv.deep_scan.scoreable_luma_mean_min
and p90_luma < cfg.cv.deep_scan.scoreable_luma_p90_min
# Standard daylight / interior shot
enough_luma = (
mean_luma >= cfg.cv.deep_scan.scoreable_luma_mean_min
or p90_luma >= cfg.cv.deep_scan.scoreable_luma_p90_min
)
return not low_visibility and contrast >= cfg.cv.deep_scan.scoreable_contrast_min
if enough_luma and contrast >= cfg.cv.deep_scan.scoreable_contrast_min:
return True
# Fade-content: dim but with structure. The local contrast must be
# well above what a uniform dim frame would have, and at least a few
# bright pixels must exist (p90 above pure-black), so we don't accept
# a featureless dark wash. These thresholds are deliberately tighter
# than the standard path so we don't pollute scoring with smooth fades.
if contrast >= 40.0 and p90_luma >= 30.0:
return True
return False
def estimate_matchable_reference_duration(