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:
+41
-6
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user