"""绿幕抠像 + 音频降噪引擎 单元测试.""" from __future__ import annotations import pytest from video_processing.chroma_key_engine import ( CHROMA_KEY_PRESETS, ChromaKeyConfig, ChromaKeyEngine, apply_chroma_key_if_needed, ) from video_processing.noise_reduction_engine import ( NoiseReductionConfig, NoiseReductionEngine, NoiseReductionLevel, apply_noise_reduction_if_needed, ) # ═══════════════════════════════════════════════════════════════ # ChromaKeyConfig 测试 # ═══════════════════════════════════════════════════════════════ class TestChromaKeyConfig: """绿幕抠像配置测试.""" def test_default_disabled(self): """默认配置是禁用的.""" config = ChromaKeyConfig() assert config.enabled is False assert config.has_effect() is False def test_from_dict_none(self): """传入 None 返回禁用配置.""" config = ChromaKeyConfig.from_dict(None) assert config.enabled is False assert config.has_effect() is False def test_from_dict_empty(self): """传入空 dict 返回禁用配置.""" config = ChromaKeyConfig.from_dict({}) assert config.enabled is False def test_from_dict_disabled(self): """enabled=false 时禁用.""" config = ChromaKeyConfig.from_dict({"enabled": False}) assert config.enabled is False assert config.has_effect() is False def test_from_dict_enabled_defaults(self): """只开启,使用默认参数.""" config = ChromaKeyConfig.from_dict({"enabled": True}) assert config.enabled is True assert config.key_color == "#00FF00" assert config.similarity == 0.3 assert config.blend == 0.1 assert config.spill_suppress == 0.0 assert config.has_effect() is True def test_from_dict_custom_params(self): """自定义所有参数.""" config = ChromaKeyConfig.from_dict( { "enabled": True, "key_color": "#0000FF", "similarity": 0.5, "blend": 0.2, "spill_suppress": 0.3, } ) assert config.key_color == "#0000FF" assert config.similarity == 0.5 assert config.blend == 0.2 assert config.spill_suppress == 0.3 def test_similarity_clamp(self): """similarity 越界自动钳制.""" # 低于最小值 config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 0}) assert config.similarity == 0.01 # 高于最大值 config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": 2.0}) assert config.similarity == 1.0 def test_blend_clamp(self): """blend 越界自动钳制.""" config = ChromaKeyConfig.from_dict({"enabled": True, "blend": -0.5}) assert config.blend == 0.0 config = ChromaKeyConfig.from_dict({"enabled": True, "blend": 2.0}) assert config.blend == 1.0 def test_spill_suppress_clamp(self): """spill_suppress 越界自动钳制.""" config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": -0.1}) assert config.spill_suppress == 0.0 config = ChromaKeyConfig.from_dict({"enabled": True, "spill_suppress": 2.0}) assert config.spill_suppress == 1.0 def test_invalid_similarity_still_works(self): """无效相似度值也能安全解析(钳制后仍有效果).""" config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": "invalid"}) # 字符串转 float 会失败 → 应该用 try/except 保护 # 实际上 from_dict 直接 float() 转换会抛异常 # 这里测试调用方的降级策略 def test_has_effect_zero_similarity(self): """similarity 为 0(被钳制到0.01)时仍然有效果.""" config = ChromaKeyConfig(enabled=True, similarity=0.0) # 注意:直接构造不走 from_dict 的钳制逻辑 assert config.similarity == 0.0 assert config.has_effect() is False # similarity > 0 # ═══════════════════════════════════════════════════════════════ # ChromaKeyEngine 测试 # ═══════════════════════════════════════════════════════════════ class TestChromaKeyEngine: """绿幕抠像引擎测试.""" def test_build_filter_basic(self): """基础抠像滤镜构建.""" config = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1) engine = ChromaKeyEngine(config) result = engine.build_filter("[0:v]", "[out]") assert "[0:v]" in result assert "[out]" in result assert "colorkey" in result assert "color=0x00FF00" in result assert "similarity=0.3" in result assert "blend=0.1" in result def test_build_filter_no_effect(self): """无效果时返回 copy.""" config = ChromaKeyConfig(enabled=False) engine = ChromaKeyEngine(config) result = engine.build_filter("[in]", "[out]") assert "copy" in result assert "colorkey" not in result def test_normalize_color_hex(self): """hex 颜色格式化.""" engine = ChromaKeyEngine(ChromaKeyConfig(enabled=True)) assert engine._normalize_color("#00FF00") == "0x00FF00" assert engine._normalize_color("#00ff00") == "0x00FF00" assert engine._normalize_color("0x00FF00") == "0X00FF00" def test_normalize_color_name(self): """颜色名直接透传.""" engine = ChromaKeyEngine(ChromaKeyConfig(enabled=True)) assert engine._normalize_color("green") == "green" assert engine._normalize_color("blue") == "blue" def test_build_filter_with_spill_suppress(self): """溢色抑制时增加 colorchannelmixer.""" config = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1, spill_suppress=0.5) engine = ChromaKeyEngine(config) result = engine.build_filter("[v0]", "[v1]") assert "colorkey" in result assert "colorchannelmixer" in result # 绿通道增益应该降低 assert "gg=" in result def test_build_filter_no_spill_suppress(self): """无溢色抑制时不含 colorchannelmixer.""" config = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1, spill_suppress=0.0) engine = ChromaKeyEngine(config) result = engine.build_filter("[v0]", "[v1]") assert "colorkey" in result assert "colorchannelmixer" not in result def test_build_filter_chromakey(self): """chromakey 滤镜构建(高级版本).""" config = ChromaKeyConfig(enabled=True, key_color="#00FF00", similarity=0.3, blend=0.1) engine = ChromaKeyEngine(config) result = engine.build_filter_chromakey("[in]", "[out]") assert "chromakey" in result assert "color=0x00FF00" in result def test_blue_screen(self): """蓝幕抠像.""" config = ChromaKeyConfig.from_dict({"enabled": True, "key_color": "#0000FF", "similarity": 0.3}) engine = ChromaKeyEngine(config) result = engine.build_filter("[0:v]", "[out]") assert "color=0x0000FF" in result # ═══════════════════════════════════════════════════════════════ # 预设测试 # ═══════════════════════════════════════════════════════════════ class TestChromaKeyPresets: """绿幕预设测试.""" def test_presets_exist(self): """预设列表包含常见预设.""" assert "green_screen" in CHROMA_KEY_PRESETS assert "blue_screen" in CHROMA_KEY_PRESETS assert "red_screen" in CHROMA_KEY_PRESETS assert "precise_green" in CHROMA_KEY_PRESETS assert "soft_green" in CHROMA_KEY_PRESETS def test_green_screen_preset_valid(self): """绿幕预设参数有效.""" preset = CHROMA_KEY_PRESETS["green_screen"] config = ChromaKeyConfig.from_dict({"enabled": True, **preset}) assert config.has_effect() is True assert config.key_color == "#00FF00" assert 0.01 <= config.similarity <= 1.0 def test_blue_screen_preset_valid(self): """蓝幕预设参数有效.""" preset = CHROMA_KEY_PRESETS["blue_screen"] config = ChromaKeyConfig.from_dict({"enabled": True, **preset}) assert config.key_color == "#0000FF" # ═══════════════════════════════════════════════════════════════ # apply_chroma_key_if_needed 测试 # ═══════════════════════════════════════════════════════════════ class TestApplyChromaKeyIfNeeded: """便捷函数测试.""" def test_no_chroma_key_in_config(self): """没有 chroma_key 配置时返回 None.""" result = apply_chroma_key_if_needed({}, "[in]", "[out]") assert result is None def test_disabled_chroma_key(self): """禁用的抠像配置返回 None.""" result = apply_chroma_key_if_needed({"chroma_key": {"enabled": False}}, "[in]", "[out]") assert result is None def test_enabled_chroma_key(self): """启用的抠像配置返回滤镜字符串.""" result = apply_chroma_key_if_needed( {"chroma_key": {"enabled": True, "key_color": "#00FF00"}}, "[v0]", "[ck0]", ) assert result is not None assert "colorkey" in result assert "[v0]" in result assert "[ck0]" in result def test_invalid_config_degrades_gracefully(self): """无效配置不抛出异常,返回 None.""" result = apply_chroma_key_if_needed( {"chroma_key": {"enabled": True, "similarity": "invalid"}}, "[in]", "[out]", ) # float("invalid") 会抛 ValueError,但 apply 函数应该捕获 # 注意:当前 from_dict 没有 try/except,调用方的 apply 应该处理 # 这里验证不会崩溃 assert result is None or isinstance(result, str) # ═══════════════════════════════════════════════════════════════ # NoiseReductionConfig 测试 # ═══════════════════════════════════════════════════════════════ class TestNoiseReductionConfig: """音频降噪配置测试.""" def test_default_disabled(self): """默认配置是禁用的.""" config = NoiseReductionConfig() assert config.enabled is False assert config.has_effect() is False def test_from_dict_none(self): """传入 None 返回禁用配置.""" config = NoiseReductionConfig.from_dict(None) assert config.enabled is False assert config.has_effect() is False def test_from_dict_empty(self): """传入空 dict 返回禁用配置.""" config = NoiseReductionConfig.from_dict({}) assert config.enabled is False def test_from_dict_enabled_default(self): """只开启,使用默认参数.""" config = NoiseReductionConfig.from_dict({"enabled": True}) assert config.enabled is True assert config.level == NoiseReductionLevel.MEDIUM assert config.has_effect() is True def test_from_dict_low_level(self): """低降噪等级.""" config = NoiseReductionConfig.from_dict({"enabled": True, "level": "low"}) assert config.level == NoiseReductionLevel.LOW assert config.get_effective_noise_floor() == -35.0 def test_from_dict_medium_level(self): """中降噪等级.""" config = NoiseReductionConfig.from_dict({"enabled": True, "level": "medium"}) assert config.level == NoiseReductionLevel.MEDIUM assert config.get_effective_noise_floor() == -25.0 def test_from_dict_high_level(self): """高降噪等级.""" config = NoiseReductionConfig.from_dict({"enabled": True, "level": "high"}) assert config.level == NoiseReductionLevel.HIGH assert config.get_effective_noise_floor() == -15.0 def test_from_dict_custom_level(self): """自定义降噪等级.""" config = NoiseReductionConfig.from_dict({"enabled": True, "level": "custom", "noise_floor": -30.0}) assert config.level == NoiseReductionLevel.CUSTOM assert config.get_effective_noise_floor() == -30.0 def test_invalid_level_falls_back_to_medium(self): """无效等级回退到 medium.""" config = NoiseReductionConfig.from_dict({"enabled": True, "level": "ultra"}) assert config.level == NoiseReductionLevel.MEDIUM def test_noise_floor_clamp(self): """noise_floor 越界自动钳制.""" # 低于最小值 config = NoiseReductionConfig.from_dict({"enabled": True, "level": "custom", "noise_floor": -100}) assert config.noise_floor == -60.0 # 高于最大值 config = NoiseReductionConfig.from_dict({"enabled": True, "level": "custom", "noise_floor": 0}) assert config.noise_floor == -5.0 def test_voice_enhance(self): """人声增强开关.""" config = NoiseReductionConfig.from_dict({"enabled": True, "voice_enhance": True}) assert config.voice_enhance is True # ═══════════════════════════════════════════════════════════════ # NoiseReductionEngine 测试 # ═══════════════════════════════════════════════════════════════ class TestNoiseReductionEngine: """音频降噪引擎测试.""" def test_build_filter_basic(self): """基础降噪滤镜构建.""" config = NoiseReductionConfig(enabled=True, level=NoiseReductionLevel.MEDIUM) engine = NoiseReductionEngine(config) result = engine.build_filter("[0:a]", "[out]") assert "[0:a]" in result assert "[out]" in result assert "afftdn" in result assert "nf=-25" in result def test_build_filter_no_effect(self): """无效果时返回 anull.""" config = NoiseReductionConfig(enabled=False) engine = NoiseReductionEngine(config) result = engine.build_filter("[in]", "[out]") assert "anull" in result assert "afftdn" not in result def test_low_level(self): """低降噪等级参数正确.""" config = NoiseReductionConfig(enabled=True, level=NoiseReductionLevel.LOW) engine = NoiseReductionEngine(config) result = engine.build_filter("[in]", "[out]") assert "nf=-35" in result def test_high_level(self): """高降噪等级参数正确.""" config = NoiseReductionConfig(enabled=True, level=NoiseReductionLevel.HIGH) engine = NoiseReductionEngine(config) result = engine.build_filter("[in]", "[out]") assert "nf=-15" in result def test_custom_level(self): """自定义降噪等级.""" config = NoiseReductionConfig(enabled=True, level=NoiseReductionLevel.CUSTOM, noise_floor=-40.0) engine = NoiseReductionEngine(config) result = engine.build_filter("[in]", "[out]") assert "nf=-40" in result def test_voice_enhance_adds_filters(self): """人声增强增加额外滤镜.""" config = NoiseReductionConfig(enabled=True, level=NoiseReductionLevel.MEDIUM, voice_enhance=True) engine = NoiseReductionEngine(config) result = engine.build_filter("[in]", "[out]") assert "afftdn" in result assert "highpass" in result assert "acompressor" in result assert "loudnorm" in result def test_no_voice_enhance_clean(self): """无人声增强时只有 afftdn.""" config = NoiseReductionConfig(enabled=True, level=NoiseReductionLevel.MEDIUM, voice_enhance=False) engine = NoiseReductionEngine(config) result = engine.build_filter("[in]", "[out]") assert "afftdn" in result assert "highpass" not in result assert "acompressor" not in result def test_arnndn_filter(self): """RNN 降噪滤镜构建.""" config = NoiseReductionConfig(enabled=True, level=NoiseReductionLevel.MEDIUM) engine = NoiseReductionEngine(config) result = engine.build_filter_arnndn("[in]", "[out]", "/models/rnnoise.rnnn") assert "arnndn" in result assert "m=/models/rnnoise.rnnn" in result # ═══════════════════════════════════════════════════════════════ # apply_noise_reduction_if_needed 测试 # ═══════════════════════════════════════════════════════════════ class TestApplyNoiseReductionIfNeeded: """便捷函数测试.""" def test_none_config(self): """None 配置返回 None.""" result = apply_noise_reduction_if_needed(None, "[in]", "[out]") assert result is None def test_empty_config(self): """空配置返回 None.""" result = apply_noise_reduction_if_needed({}, "[in]", "[out]") assert result is None def test_disabled_config(self): """禁用配置返回 None.""" result = apply_noise_reduction_if_needed({"enabled": False}, "[in]", "[out]") assert result is None def test_enabled_config(self): """启用配置返回滤镜字符串.""" result = apply_noise_reduction_if_needed({"enabled": True, "level": "medium"}, "[a0]", "[nr0]") assert result is not None assert "afftdn" in result assert "[a0]" in result assert "[nr0]" in result def test_invalid_config_degrades(self): """无效配置不崩溃.""" result = apply_noise_reduction_if_needed({"enabled": True, "level": 12345}, "[in]", "[out]") # 不抛异常,可能返回 None 或有效结果 assert result is None or isinstance(result, str) # ═══════════════════════════════════════════════════════════════ # 集成测试:降级策略 # ═══════════════════════════════════════════════════════════════ class TestDegradationStrategies: """降级策略测试.""" def test_chroma_key_none_config_safe(self): """绿幕:None 配置安全.""" # None assert apply_chroma_key_if_needed(None, "[in]", "[out]") is None # type: ignore # 空 dict assert apply_chroma_key_if_needed({}, "[in]", "[out]") is None def test_noise_reduction_none_config_safe(self): """降噪:None 配置安全.""" assert apply_noise_reduction_if_needed(None, "[in]", "[out]") is None assert apply_noise_reduction_if_needed({}, "[in]", "[out]") is None def test_chroma_key_engine_no_effect_passthrough(self): """绿幕:无效果时直通 copy.""" config = ChromaKeyConfig(enabled=False) engine = ChromaKeyEngine(config) result = engine.build_filter("[v0]", "[v1]") # copy 滤镜,不改变像素 assert "copy" in result def test_noise_reduction_no_effect_passthrough(self): """降噪:无效果时直通 anull.""" config = NoiseReductionConfig(enabled=False) engine = NoiseReductionEngine(config) result = engine.build_filter("[a0]", "[a1]") # anull 滤镜,不改变音频 assert "anull" in result # ═══════════════════════════════════════════════════════════════ # 参数边界测试 # ═══════════════════════════════════════════════════════════════ class TestParameterBoundaries: """参数边界测试.""" @pytest.mark.parametrize( "similarity,expected", [ (0.0, 0.01), # 低于最小值 → 钳制到 min (0.01, 0.01), # 最小值 (0.5, 0.5), # 中间值 (1.0, 1.0), # 最大值 (2.0, 1.0), # 超过最大值 → 钳制到 max ], ) def test_similarity_boundaries(self, similarity, expected): """similarity 边界值测试.""" config = ChromaKeyConfig.from_dict({"enabled": True, "similarity": similarity}) assert abs(config.similarity - expected) < 0.001 @pytest.mark.parametrize( "blend,expected", [ (-1.0, 0.0), (0.0, 0.0), (0.5, 0.5), (1.0, 1.0), (2.0, 1.0), ], ) def test_blend_boundaries(self, blend, expected): """blend 边界值测试.""" config = ChromaKeyConfig.from_dict({"enabled": True, "blend": blend}) assert abs(config.blend - expected) < 0.001 @pytest.mark.parametrize( "noise_floor,expected", [ (-100, -60.0), (-60, -60.0), (-30, -30.0), (-5, -5.0), (0, -5.0), ], ) def test_noise_floor_boundaries(self, noise_floor, expected): """noise_floor 边界值测试.""" config = NoiseReductionConfig.from_dict({"enabled": True, "level": "custom", "noise_floor": noise_floor}) assert abs(config.noise_floor - expected) < 0.001