diff --git a/media_shrinker.py b/media_shrinker.py index d72a8d5..3573eb7 100644 --- a/media_shrinker.py +++ b/media_shrinker.py @@ -84,8 +84,8 @@ BITRATE_SAFETY_MARGIN = 0.92 OPUS_MAX_BITRATE_BPS = 510_000 OPUS_MIN_REASONABLE_BITRATE_BPS = 16_000 -SILENCE_START_RE = re.compile(r"silence_start:\s*(?P[0-9]+(?:\.[0-9]+)?)") -SILENCE_END_RE = re.compile(r"silence_end:\s*(?P[0-9]+(?:\.[0-9]+)?)") +SILENCE_START_RE = re.compile(r"silence_start:\s*(?P-?[0-9]+(?:\.[0-9]+)?)") +SILENCE_END_RE = re.compile(r"silence_end:\s*(?P-?[0-9]+(?:\.[0-9]+)?)") class MediaShrinkerError(RuntimeError): diff --git a/tests/test_media_shrinker.py b/tests/test_media_shrinker.py index 0ab2397..8936e02 100644 --- a/tests/test_media_shrinker.py +++ b/tests/test_media_shrinker.py @@ -915,6 +915,39 @@ def test_parse_silencedetect_intervals_pairs_long_silence_start_and_end( ], ) + def test_parse_silencedetect_intervals_keeps_negative_start_at_recording_head( + self, + ) -> None: + # ffmpeg back-dates silence_start by the detection window, so a file that + # begins in silence reports a slightly negative silence_start. The whole + # interval must survive so it can still be used as a split boundary. + stderr = """ + [silencedetect @ 0x1] silence_start: -0.00816327 + [silencedetect @ 0x1] silence_end: 150.5 | silence_duration: 150.5 + """ + + intervals = parse_silencedetect_intervals(stderr) + + self.assertEqual( + intervals, + [SilenceInterval(start_seconds=-0.00816327, end_seconds=150.5)], + ) + + def test_negative_start_silence_still_drives_split_point(self) -> None: + # A silence that opens the recording but extends well past the midpoint is + # the best split boundary; dropping it forces an unwanted mid-audio cut. + intervals = parse_silencedetect_intervals( + "[silencedetect @ 0x1] silence_start: -0.008\n" + "[silencedetect @ 0x1] silence_end: 150.5 | silence_duration: 150.5\n" + ) + segments = build_segments( + duration_seconds=300.0, + max_segment_duration_seconds=200.0, + silence_intervals=intervals, + ) + + self.assertEqual(segments[0].duration_seconds, 150.5) + class SilenceDetectionTests(unittest.TestCase): @patch("media_shrinker.subprocess.run")