diff --git a/scenedetect/detectors/adaptive_detector.py b/scenedetect/detectors/adaptive_detector.py index 2e98bbf4..8f3f5800 100644 --- a/scenedetect/detectors/adaptive_detector.py +++ b/scenedetect/detectors/adaptive_detector.py @@ -136,7 +136,8 @@ def process_frame(self, timecode: FrameTimecode, frame_img: np.ndarray) -> list[ threshold_met: bool = ( adaptive_ratio >= self.adaptive_threshold and target_score >= self.min_content_val ) - min_length_met: bool = (timecode - self._last_cut) >= self.min_scene_len + # Cuts are emitted at `target_timecode` (`window_width` behind `timecode`). + min_length_met: bool = (target_timecode - self._last_cut) >= self.min_scene_len if threshold_met and min_length_met: self._last_cut = target_timecode return [target_timecode] diff --git a/tests/test_detectors.py b/tests/test_detectors.py index a6a9f283..6754d910 100644 --- a/tests/test_detectors.py +++ b/tests/test_detectors.py @@ -19,6 +19,7 @@ import os from dataclasses import dataclass +import numpy as np import pytest from scenedetect import FrameTimecode, SceneDetector, SceneManager, StatsManager, detect @@ -261,3 +262,33 @@ def test_min_scene_len_accepts_time_values(detector_type, min_scene_len): scene_list = test_case.detect() start_frames = [timecode.frame_num for timecode, _ in scene_list] assert start_frames == test_case.scene_boundaries + + +def test_adaptive_detector_min_scene_len_uses_target_frame(): + """AdaptiveDetector applies min_scene_len to the emitted cut, not the current frame. + + AdaptiveDetector scores a target frame `window_width` behind the frame currently + being processed. Comparing min_scene_len against the current frame lets a second + peak emit a cut only `window_width` frames after the previous one (issue #408). + """ + window_width = 20 + min_scene_len = 24 + detector = AdaptiveDetector( + adaptive_threshold=2.0, + window_width=window_width, + min_scene_len=min_scene_len, + min_content_val=15.0, + luma_only=True, + ) + fps = 30.0 + n_frames = 180 + cuts: list[FrameTimecode] = [] + for frame_num in range(n_frames): + # Hard cuts at 100 and 104 (4 frames apart) plus a later valid cut at 140. + # The 4-frame pair matches the report: min_scene_len - window_width. + value = 255 if 100 <= frame_num <= 103 or 140 <= frame_num <= 143 else 0 + frame = np.full((16, 16, 3), value, dtype=np.uint8) + cuts.extend(detector.process_frame(FrameTimecode(frame_num, fps), frame)) + + cut_frames = [cut.frame_num for cut in cuts] + assert cut_frames == [100, 140] diff --git a/website/pages/changelog.md b/website/pages/changelog.md index 7789e5c8..1cf9920a 100644 --- a/website/pages/changelog.md +++ b/website/pages/changelog.md @@ -782,6 +782,7 @@ Development ## PySceneDetect 0.7.2 (TBD) + - [bugfix] Fix `AdaptiveDetector` / `detect-adaptive` emitting scenes shorter than `min_scene_len` when `window_width` is large, by applying the minimum-length check to the emitted (target) frame instead of the current frame [#408](https://github.com/Breakthrough/PySceneDetect/issues/408) - [general] The `scenedetect-core` package introduced in 0.7.1 has been discontinued, and its only release (0.7.1) yanked from PyPI: pip cannot safely support multiple packages that install the same module files, and restructuring the existing packages around a shared core would break in-place upgrades. Existing `scenedetect-core` installs keep working but will not receive updates; continue to install `scenedetect` or `scenedetect-headless` as usual. - [improvement] `HistogramDetector` (`detect-hist`) default `threshold` changed from 0.05 to 0.20 and default `bins` from 256 to 128, calibrated from the [benchmark sweep](https://www.scenedetect.com/benchmarks/) for significantly better accuracy. Default output for this detector will change [#559](https://github.com/Breakthrough/PySceneDetect/issues/559) - [improvement] `HashDetector` (`detect-hash`) default `threshold` changed from 0.395 to 0.35 and default `size` from 16 to 8, calibrated from the [benchmark sweep](https://www.scenedetect.com/benchmarks/) for better accuracy. Default output for this detector will change, including the statsfile metric key (now `hash_dist [size=8 lowpass=2]`) [#559](https://github.com/Breakthrough/PySceneDetect/issues/559)