"""speed_config 调速配置领域模型单测.""" import pytest from domain.speed_config import ( DEFAULT_SPEED, MAX_SPEED, MIN_SPEED, SpeedConfig, adjust_duration, build_audio_filter, build_clip_speed_filter, build_video_filter, resolve_clip_speed, ) # ── 常量测试 ───────────────────────────────────────────────────────────────── class TestConstants: """模块常量""" def test_speed_limits(self): assert MIN_SPEED == 0.25 assert MAX_SPEED == 4.0 assert DEFAULT_SPEED == 1.0 # ── SpeedConfig 默认值与基础 ──────────────────────────────────────────────── class TestSpeedConfigDefaults: """SpeedConfig 默认值""" def test_default_values(self): c = SpeedConfig() assert c.speed == 1.0 assert c.pitch_correct is True def test_custom_values(self): c = SpeedConfig(speed=2.0, pitch_correct=False) assert c.speed == 2.0 assert c.pitch_correct is False # ── SpeedConfig.parse ─────────────────────────────────────────────────────── class TestSpeedConfigParse: """SpeedConfig.parse 工厂方法""" def test_none_returns_default(self): c = SpeedConfig.parse(None) assert c.speed == 1.0 assert c.pitch_correct is True def test_empty_dict_returns_default(self): c = SpeedConfig.parse({}) assert c.speed == 1.0 def test_not_dict_returns_default(self): c = SpeedConfig.parse("not a dict") assert c.speed == 1.0 def test_valid_speed(self): c = SpeedConfig.parse({"speed": 2.0}) assert c.speed == 2.0 def test_valid_speed_int(self): c = SpeedConfig.parse({"speed": 2}) assert c.speed == 2.0 assert isinstance(c.speed, float) def test_pitch_correct_false(self): c = SpeedConfig.parse({"pitch_correct": False}) assert c.pitch_correct is False def test_pitch_correct_non_bool_falls_back(self): c = SpeedConfig.parse({"pitch_correct": "true"}) assert c.pitch_correct is True def test_invalid_speed_string_falls_back(self): c = SpeedConfig.parse({"speed": "fast"}) assert c.speed == 1.0 def test_speed_below_min_clamped(self): c = SpeedConfig.parse({"speed": 0.1}) assert c.speed == MIN_SPEED def test_speed_above_max_clamped(self): c = SpeedConfig.parse({"speed": 10.0}) assert c.speed == MAX_SPEED def test_zero_speed_falls_back_to_default(self): c = SpeedConfig.parse({"speed": 0}) assert c.speed == DEFAULT_SPEED def test_negative_speed_falls_back(self): c = SpeedConfig.parse({"speed": -1.0}) assert c.speed == DEFAULT_SPEED def test_min_speed_boundary(self): c = SpeedConfig.parse({"speed": 0.25}) assert c.speed == 0.25 def test_max_speed_boundary(self): c = SpeedConfig.parse({"speed": 4.0}) assert c.speed == 4.0 # ── SpeedConfig.clamp ─────────────────────────────────────────────────────── class TestSpeedConfigClamp: """SpeedConfig.clamp 方法""" def test_normal_speed_no_change(self): c = SpeedConfig(speed=1.5) c.clamp() assert c.speed == 1.5 def test_zero_speed_reset_default(self): c = SpeedConfig(speed=0.0) c.clamp() assert c.speed == DEFAULT_SPEED def test_negative_speed_reset_default(self): c = SpeedConfig(speed=-0.5) c.clamp() assert c.speed == DEFAULT_SPEED def test_below_min_clamped(self): c = SpeedConfig(speed=0.1) c.clamp() assert c.speed == MIN_SPEED def test_above_max_clamped(self): c = SpeedConfig(speed=5.0) c.clamp() assert c.speed == MAX_SPEED def test_exact_min_unchanged(self): c = SpeedConfig(speed=MIN_SPEED) c.clamp() assert c.speed == MIN_SPEED def test_exact_max_unchanged(self): c = SpeedConfig(speed=MAX_SPEED) c.clamp() assert c.speed == MAX_SPEED # ── SpeedConfig 属性方法 ──────────────────────────────────────────────────── class TestSpeedConfigProperties: """SpeedConfig 属性方法""" def test_is_original_true(self): c = SpeedConfig(speed=1.0) assert c.is_original is True def test_is_original_very_close(self): c = SpeedConfig(speed=1.0 + 1e-7) assert c.is_original is True def test_is_original_false_fast(self): c = SpeedConfig(speed=1.5) assert c.is_original is False def test_is_original_false_slow(self): c = SpeedConfig(speed=0.8) assert c.is_original is False def test_is_fast_true(self): c = SpeedConfig(speed=2.0) assert c.is_fast is True def test_is_fast_false(self): c = SpeedConfig(speed=0.5) assert c.is_fast is False def test_is_fast_at_one(self): c = SpeedConfig(speed=1.0) assert c.is_fast is False def test_is_slow_true(self): c = SpeedConfig(speed=0.5) assert c.is_slow is True def test_is_slow_false(self): c = SpeedConfig(speed=2.0) assert c.is_slow is False def test_is_slow_at_one(self): c = SpeedConfig(speed=1.0) assert c.is_slow is False # ── build_video_filter ─────────────────────────────────────────────────────── class TestBuildVideoFilter: """build_video_filter 视频滤镜构建""" def test_original_speed_empty(self): c = SpeedConfig(speed=1.0) assert build_video_filter(c) == "" def test_double_speed(self): c = SpeedConfig(speed=2.0) result = build_video_filter(c) assert "setpts=PTS/2.0" in result def test_half_speed(self): c = SpeedConfig(speed=0.5) result = build_video_filter(c) assert "setpts=PTS/0.5" in result def test_format_precision(self): c = SpeedConfig(speed=1.5) result = build_video_filter(c) # 应该是 4 位小数 assert "1.5000" in result def test_min_speed(self): c = SpeedConfig(speed=0.25) result = build_video_filter(c) assert result.startswith("setpts=PTS/") def test_max_speed(self): c = SpeedConfig(speed=4.0) result = build_video_filter(c) assert "4.0000" in result # ── build_audio_filter / atempo 拆分 ──────────────────────────────────────── class TestBuildAudioFilter: """build_audio_filter 音频滤镜构建""" def test_original_speed_empty(self): c = SpeedConfig(speed=1.0) assert build_audio_filter(c) == "" def test_within_range_single_stage(self): c = SpeedConfig(speed=1.5) result = build_audio_filter(c) assert result == "atempo=1.5000" def test_05_speed_single_stage(self): c = SpeedConfig(speed=0.5) result = build_audio_filter(c) assert result == "atempo=0.5000" def test_20_speed_single_stage(self): c = SpeedConfig(speed=2.0) result = build_audio_filter(c) assert result == "atempo=2.0000" def test_4x_speed_two_stages(self): c = SpeedConfig(speed=4.0) result = build_audio_filter(c) # 2.0 * 2.0 = 4.0 assert result == "atempo=2.0000,atempo=2.0000" def test_025_speed_two_stages(self): c = SpeedConfig(speed=0.25) result = build_audio_filter(c) # 0.5 * 0.5 = 0.25 assert result == "atempo=0.5000,atempo=0.5000" def test_3x_speed_two_stages(self): c = SpeedConfig(speed=3.0) result = build_audio_filter(c) # 2.0 * 1.5 = 3.0 stages = result.split(",") assert len(stages) == 2 assert "atempo=2.0000" in stages[0] assert "atempo=1.5000" in stages[1] def test_03_speed_two_stages(self): c = SpeedConfig(speed=0.3) result = build_audio_filter(c) stages = result.split(",") assert len(stages) == 2 # 0.5 * 0.6 = 0.3 assert "atempo=0.5000" in stages[0] def test_format_each_stage(self): c = SpeedConfig(speed=1.2345) result = build_audio_filter(c) assert "atempo=1.2345" in result class TestAtempoStages: """atempo 多级拆分逻辑验证""" def _extract_speeds(self, filter_str: str) -> list[float]: """从 atempo 滤镜字符串中提取速度值.""" import re return [float(m) for m in re.findall(r"atempo=([\d.]+)", filter_str)] def test_product_equals_speed_fast_3x(self): c = SpeedConfig(speed=3.0) speeds = self._extract_speeds(build_audio_filter(c)) product = 1.0 for s in speeds: product *= s assert abs(product - 3.0) < 1e-4 def test_product_equals_speed_4x(self): c = SpeedConfig(speed=4.0) speeds = self._extract_speeds(build_audio_filter(c)) product = 1.0 for s in speeds: product *= s assert abs(product - 4.0) < 1e-4 def test_product_equals_speed_slow_025(self): c = SpeedConfig(speed=0.25) speeds = self._extract_speeds(build_audio_filter(c)) product = 1.0 for s in speeds: product *= s assert abs(product - 0.25) < 1e-4 def test_product_equals_speed_slow_03(self): c = SpeedConfig(speed=0.3) speeds = self._extract_speeds(build_audio_filter(c)) product = 1.0 for s in speeds: product *= s assert abs(product - 0.3) < 1e-4 def test_each_stage_in_range_fast(self): c = SpeedConfig(speed=3.5) speeds = self._extract_speeds(build_audio_filter(c)) for s in speeds: assert 0.5 <= s <= 2.0 def test_each_stage_in_range_slow(self): c = SpeedConfig(speed=0.35) speeds = self._extract_speeds(build_audio_filter(c)) for s in speeds: assert 0.5 <= s <= 2.0 # ── adjust_duration ────────────────────────────────────────────────────────── class TestAdjustDuration: """adjust_duration 时长计算""" def test_original_speed_no_change(self): assert adjust_duration(10.0, SpeedConfig(speed=1.0)) == 10.0 def test_double_speed_half_duration(self): assert adjust_duration(10.0, SpeedConfig(speed=2.0)) == 5.0 def test_half_speed_double_duration(self): assert adjust_duration(10.0, SpeedConfig(speed=0.5)) == 20.0 def test_zero_duration_unchanged(self): assert adjust_duration(0.0, SpeedConfig(speed=2.0)) == 0.0 def test_negative_duration_unchanged(self): assert adjust_duration(-1.0, SpeedConfig(speed=2.0)) == -1.0 def test_original_with_zero_duration(self): assert adjust_duration(0.0, SpeedConfig(speed=1.0)) == 0.0 def test_triple_speed(self): assert adjust_duration(30.0, SpeedConfig(speed=3.0)) == 10.0 def test_quarter_speed(self): assert adjust_duration(10.0, SpeedConfig(speed=0.25)) == 40.0 # ── build_clip_speed_filter ────────────────────────────────────────────────── class TestBuildClipSpeedFilter: """build_clip_speed_filter 便捷方法""" def test_returns_tuple_of_three(self): result = build_clip_speed_filter(1.5) assert len(result) == 3 video_filter, audio_filter, config = result assert isinstance(video_filter, str) assert isinstance(audio_filter, str) assert isinstance(config, SpeedConfig) def test_normal_speed(self): video_filter, audio_filter, config = build_clip_speed_filter(1.0) assert video_filter == "" assert audio_filter == "" assert config.speed == 1.0 def test_double_speed(self): video_filter, audio_filter, config = build_clip_speed_filter(2.0) assert "setpts" in video_filter assert "atempo" in audio_filter assert config.speed == 2.0 def test_clamps_speed(self): _, _, config = build_clip_speed_filter(10.0) assert config.speed == MAX_SPEED def test_pitch_correct_false(self): # pitch_correct=False 时仍然生成滤镜(实际使用中可能换其他算法,但接口返回不变) video_filter, audio_filter, config = build_clip_speed_filter(2.0, pitch_correct=False) assert config.pitch_correct is False assert "setpts" in video_filter # ── resolve_clip_speed ─────────────────────────────────────────────────────── class TestResolveClipSpeed: """resolve_clip_speed 片段速度解析""" def test_none_config_uses_global(self): assert resolve_clip_speed(None, 1.5) == 1.5 def test_zero_speed_uses_global(self): assert resolve_clip_speed({"playback_speed": 0}, 1.5) == 1.5 def test_missing_key_uses_global(self): assert resolve_clip_speed({}, 2.0) == 2.0 def test_valid_speed(self): assert resolve_clip_speed({"playback_speed": 1.5}, 1.0) == 1.5 def test_negative_speed_uses_global(self): assert resolve_clip_speed({"playback_speed": -1.0}, 1.0) == 1.0 def test_invalid_type_uses_global(self): assert resolve_clip_speed({"playback_speed": "fast"}, 1.0) == 1.0 def test_default_global_is_one(self): assert resolve_clip_speed({"playback_speed": 0}) == 1.0 def test_int_speed(self): result = resolve_clip_speed({"playback_speed": 2}) assert result == 2.0 assert isinstance(result, float) def test_very_small_positive_uses_it(self): # 只要 > 0 就用 result = resolve_clip_speed({"playback_speed": 0.1}) assert result == 0.1