From 7a72cfd709d8be01f103013c8537452fd69b71d7 Mon Sep 17 00:00:00 2001 From: CI Bot Date: Sat, 25 Jul 2026 00:16:10 +0000 Subject: [PATCH] style: auto-format with black + isort + prettier --- tests/unit/test_asr_service_factory.py | 1 - tests/unit/test_bgm_mixer.py | 54 ++-- tests/unit/test_chroma_key_engine.py | 117 ++++---- tests/unit/test_color_grade_engine.py | 89 ++++--- tests/unit/test_concat_engine.py | 231 +++++++++------- tests/unit/test_ffmpeg_pure_utils.py | 7 +- tests/unit/test_intro_outro_engine.py | 1 - tests/unit/test_module_registry.py | 56 ++-- tests/unit/test_multi_track_mixer.py | 309 +++++++++++++--------- tests/unit/test_noise_reduction_engine.py | 83 +++--- tests/unit/test_oss_helpers_pure.py | 13 +- tests/unit/test_pip_engine.py | 103 ++++---- tests/unit/test_render_adapter_pure.py | 1 - tests/unit/test_render_audio_pure.py | 1 - tests/unit/test_speed_engine.py | 2 - tests/unit/test_sticker_engine.py | 25 +- tests/unit/test_subtitle_render_engine.py | 12 +- tests/unit/test_templates_editor_utils.py | 1 - tests/unit/test_thumbnail_generator.py | 1 - tests/unit/test_transition_engine.py | 1 - tests/unit/test_trim_engine.py | 62 +++-- tests/unit/test_unified_render_pure.py | 11 +- tests/unit/test_watermark_engine.py | 232 ++++++++-------- 23 files changed, 788 insertions(+), 625 deletions(-) diff --git a/tests/unit/test_asr_service_factory.py b/tests/unit/test_asr_service_factory.py index bec54b2e8..b7536edda 100755 --- a/tests/unit/test_asr_service_factory.py +++ b/tests/unit/test_asr_service_factory.py @@ -5,7 +5,6 @@ from __future__ import annotations import os import pytest - from services.asr_service_factory import get_asr_service, reset_asr_service_cache diff --git a/tests/unit/test_bgm_mixer.py b/tests/unit/test_bgm_mixer.py index b9531c656..6855eac39 100755 --- a/tests/unit/test_bgm_mixer.py +++ b/tests/unit/test_bgm_mixer.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.bgm_mixer import BGMConfig @@ -43,10 +42,13 @@ class TestBGMConfigFromConfigDict: def test_fade_in_out(self): """淡入淡出.""" - config = BGMConfig.from_config_dict("/a.mp3", { - "fade_in": 2.0, - "fade_out": 3.0, - }) + config = BGMConfig.from_config_dict( + "/a.mp3", + { + "fade_in": 2.0, + "fade_out": 3.0, + }, + ) assert config.fade_in == 2.0 assert config.fade_out == 3.0 @@ -62,13 +64,16 @@ class TestBGMConfigFromConfigDict: def test_sidechain_custom_params(self): """闪避自定义参数.""" - config = BGMConfig.from_config_dict("/a.mp3", { - "sidechain_enabled": True, - "sidechain_ratio": 0.5, - "sidechain_attack": 0.05, - "sidechain_release": 0.8, - "sidechain_threshold": -30.0, - }) + config = BGMConfig.from_config_dict( + "/a.mp3", + { + "sidechain_enabled": True, + "sidechain_ratio": 0.5, + "sidechain_attack": 0.05, + "sidechain_release": 0.8, + "sidechain_threshold": -30.0, + }, + ) assert config.sidechain_ratio == 0.5 assert config.sidechain_attack == 0.05 assert config.sidechain_release == 0.8 @@ -81,17 +86,20 @@ class TestBGMConfigFromConfigDict: def test_all_params_custom(self): """所有参数自定义.""" - config = BGMConfig.from_config_dict("/full.mp3", { - "volume": 0.7, - "fade_in": 1.5, - "fade_out": 2.0, - "loop_enabled": False, - "sidechain_enabled": True, - "sidechain_ratio": 0.4, - "sidechain_attack": 0.03, - "sidechain_release": 0.6, - "sidechain_threshold": -20.0, - }) + config = BGMConfig.from_config_dict( + "/full.mp3", + { + "volume": 0.7, + "fade_in": 1.5, + "fade_out": 2.0, + "loop_enabled": False, + "sidechain_enabled": True, + "sidechain_ratio": 0.4, + "sidechain_attack": 0.03, + "sidechain_release": 0.6, + "sidechain_threshold": -20.0, + }, + ) assert config.volume == 0.7 assert config.fade_in == 1.5 assert config.fade_out == 2.0 diff --git a/tests/unit/test_chroma_key_engine.py b/tests/unit/test_chroma_key_engine.py index d84bd0948..b3d2e66ab 100755 --- a/tests/unit/test_chroma_key_engine.py +++ b/tests/unit/test_chroma_key_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.chroma_key_engine import ( CHROMA_KEY_PRESETS, ChromaKeyConfig, @@ -52,93 +51,115 @@ class TestChromaKeyConfigFromDict: def test_custom_key_color(self): """自定义抠像颜色.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "key_color": "#0000FF", - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "key_color": "#0000FF", + } + ) assert config.key_color == "#0000FF" def test_similarity_parsed(self): """相似度解析.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "similarity": 0.5, - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "similarity": 0.5, + } + ) assert config.similarity == 0.5 def test_similarity_clamped_min(self): """相似度下限钳制.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "similarity": 0.001, - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "similarity": 0.001, + } + ) assert config.similarity == 0.01 def test_similarity_clamped_max(self): """相似度上限钳制.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "similarity": 2.0, - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "similarity": 2.0, + } + ) assert config.similarity == 1.0 def test_blend_clamped_min(self): """混合度下限钳制.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "blend": -0.5, - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "blend": -0.5, + } + ) assert config.blend == 0.0 def test_blend_clamped_max(self): """混合度上限钳制.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "blend": 1.5, - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "blend": 1.5, + } + ) assert config.blend == 1.0 def test_spill_suppress_clamped(self): """溢色抑制钳制.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "spill_suppress": 2.0, - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "spill_suppress": 2.0, + } + ) assert config.spill_suppress == 1.0 def test_invalid_similarity_falls_back(self): """无效相似度回退到默认.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "similarity": "not_a_number", - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "similarity": "not_a_number", + } + ) assert config.similarity == 0.3 def test_invalid_blend_falls_back(self): """无效混合度回退.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "blend": "high", - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "blend": "high", + } + ) assert config.blend == 0.1 def test_key_color_stripped(self): """颜色值去除首尾空格.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "key_color": " #FF0000 ", - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "key_color": " #FF0000 ", + } + ) assert config.key_color == "#FF0000" def test_all_params_custom(self): """所有参数自定义.""" - config = ChromaKeyConfig.from_dict({ - "enabled": True, - "key_color": "#0000FF", - "similarity": 0.45, - "blend": 0.15, - "spill_suppress": 0.6, - }) + config = ChromaKeyConfig.from_dict( + { + "enabled": True, + "key_color": "#0000FF", + "similarity": 0.45, + "blend": 0.15, + "spill_suppress": 0.6, + } + ) assert config.enabled is True assert config.key_color == "#0000FF" assert config.similarity == 0.45 diff --git a/tests/unit/test_color_grade_engine.py b/tests/unit/test_color_grade_engine.py index f7e2bbde3..1d56d6327 100755 --- a/tests/unit/test_color_grade_engine.py +++ b/tests/unit/test_color_grade_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.color_grade_engine import ( DEFAULT_PARAMS, PARAM_RANGES, @@ -55,39 +54,47 @@ class TestColorGradeConfigFromDict: def test_with_preset(self): """指定预设.""" - config = ColorGradeConfig.from_dict({ - "enabled": True, - "preset": "fresh", - }) + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "preset": "fresh", + } + ) assert config.enabled is True assert config.preset == "fresh" def test_invalid_preset_ignored(self): """无效预设被忽略.""" - config = ColorGradeConfig.from_dict({ - "enabled": True, - "preset": "unknown_preset", - }) + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "preset": "unknown_preset", + } + ) assert config.preset == "" def test_custom_brightness(self): """自定义亮度.""" - config = ColorGradeConfig.from_dict({ - "enabled": True, - "brightness": 20, - }) + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "brightness": 20, + } + ) assert config.brightness == 20.0 def test_custom_all_params(self): """所有参数自定义.""" - config = ColorGradeConfig.from_dict({ - "enabled": True, - "brightness": 10, - "contrast": 15, - "saturation": 120, - "temperature": -5, - "hue": 10, - }) + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "brightness": 10, + "contrast": 15, + "saturation": 120, + "temperature": -5, + "hue": 10, + } + ) assert config.brightness == 10.0 assert config.contrast == 15.0 assert config.saturation == 120.0 @@ -96,27 +103,33 @@ class TestColorGradeConfigFromDict: def test_invalid_param_value_returns_none(self): """无效参数值返回None(不覆盖).""" - config = ColorGradeConfig.from_dict({ - "enabled": True, - "brightness": "not_a_number", - }) + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "brightness": "not_a_number", + } + ) assert config.brightness is None def test_null_param_returns_none(self): """null参数值返回None.""" - config = ColorGradeConfig.from_dict({ - "enabled": True, - "contrast": None, - }) + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "contrast": None, + } + ) assert config.contrast is None def test_preset_with_custom_override(self): """预设 + 自定义覆盖.""" - config = ColorGradeConfig.from_dict({ - "enabled": True, - "preset": "vintage", - "brightness": 5, - }) + config = ColorGradeConfig.from_dict( + { + "enabled": True, + "preset": "vintage", + "brightness": 5, + } + ) assert config.preset == "vintage" assert config.brightness == 5.0 @@ -192,9 +205,7 @@ class TestResolveParams: """返回所有5个参数.""" config = ColorGradeConfig(enabled=True) params = config.resolve_params() - assert set(params.keys()) == { - "brightness", "contrast", "saturation", "temperature", "hue" - } + assert set(params.keys()) == {"brightness", "contrast", "saturation", "temperature", "hue"} class TestHasEffect: @@ -247,6 +258,4 @@ class TestPresets: def test_param_ranges_defined(self): """参数范围定义完整.""" - assert set(PARAM_RANGES.keys()) == { - "brightness", "contrast", "saturation", "temperature", "hue" - } + assert set(PARAM_RANGES.keys()) == {"brightness", "contrast", "saturation", "temperature", "hue"} diff --git a/tests/unit/test_concat_engine.py b/tests/unit/test_concat_engine.py index 5894c6d01..c25a4a2ee 100755 --- a/tests/unit/test_concat_engine.py +++ b/tests/unit/test_concat_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.concat_engine import ConcatConfig, ConcatSegment @@ -31,68 +30,84 @@ class TestConcatSegmentFromDict: def test_custom_start_time(self): """自定义开始时间.""" - seg = ConcatSegment.from_dict({ - "video_path": "/a.mp4", - "start_time": 5.0, - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/a.mp4", + "start_time": 5.0, + } + ) assert seg.start_time == 5.0 def test_custom_duration(self): """自定义时长.""" - seg = ConcatSegment.from_dict({ - "video_path": "/a.mp4", - "duration": 10.0, - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/a.mp4", + "duration": 10.0, + } + ) assert seg.duration == 10.0 def test_start_time_negative_clamped(self): """负开始时间钳制到0.""" - seg = ConcatSegment.from_dict({ - "video_path": "/a.mp4", - "start_time": -5.0, - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/a.mp4", + "start_time": -5.0, + } + ) assert seg.start_time == 0.0 def test_duration_negative_clamped(self): """负时长钳制到0.""" - seg = ConcatSegment.from_dict({ - "video_path": "/a.mp4", - "duration": -3.0, - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/a.mp4", + "duration": -3.0, + } + ) assert seg.duration == 0.0 def test_invalid_start_time_falls_back(self): """无效start_time回退到0.""" - seg = ConcatSegment.from_dict({ - "video_path": "/a.mp4", - "start_time": "invalid", - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/a.mp4", + "start_time": "invalid", + } + ) assert seg.start_time == 0.0 def test_invalid_duration_falls_back(self): """无效duration回退到0.""" - seg = ConcatSegment.from_dict({ - "video_path": "/a.mp4", - "duration": "not_a_number", - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/a.mp4", + "duration": "not_a_number", + } + ) assert seg.duration == 0.0 def test_no_audio(self): """无音频.""" - seg = ConcatSegment.from_dict({ - "video_path": "/a.mp4", - "has_audio": False, - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/a.mp4", + "has_audio": False, + } + ) assert seg.has_audio is False def test_full_config(self): """完整配置.""" - seg = ConcatSegment.from_dict({ - "video_path": "/video.mp4", - "start_time": 2.5, - "duration": 15.0, - "has_audio": False, - }) + seg = ConcatSegment.from_dict( + { + "video_path": "/video.mp4", + "start_time": 2.5, + "duration": 15.0, + "has_audio": False, + } + ) assert seg.video_path == "/video.mp4" assert seg.start_time == 2.5 assert seg.duration == 15.0 @@ -129,96 +144,116 @@ class TestConcatConfigFromConfigDict: def test_single_segment(self): """单片段.""" - config = ConcatConfig.from_config_dict({ - "segments": [{"video_path": "/a.mp4"}], - }) + config = ConcatConfig.from_config_dict( + { + "segments": [{"video_path": "/a.mp4"}], + } + ) assert len(config.segments) == 1 assert config.segments[0].video_path == "/a.mp4" def test_multiple_segments(self): """多片段.""" - config = ConcatConfig.from_config_dict({ - "segments": [ - {"video_path": "/a.mp4", "start_time": 1.0}, - {"video_path": "/b.mp4", "duration": 5.0}, - {"video_path": "/c.mp4"}, - ], - }) + config = ConcatConfig.from_config_dict( + { + "segments": [ + {"video_path": "/a.mp4", "start_time": 1.0}, + {"video_path": "/b.mp4", "duration": 5.0}, + {"video_path": "/c.mp4"}, + ], + } + ) assert len(config.segments) == 3 assert config.segments[0].start_time == 1.0 assert config.segments[1].duration == 5.0 def test_skips_no_path(self): """跳过无video_path的片段.""" - config = ConcatConfig.from_config_dict({ - "segments": [ - {"video_path": "/a.mp4"}, - {"other": "value"}, - {"video_path": ""}, - ], - }) + config = ConcatConfig.from_config_dict( + { + "segments": [ + {"video_path": "/a.mp4"}, + {"other": "value"}, + {"video_path": ""}, + ], + } + ) assert len(config.segments) == 1 def test_segments_not_list_ignored(self): """segments不是列表忽略.""" - config = ConcatConfig.from_config_dict({ - "segments": "not_a_list", - }) + config = ConcatConfig.from_config_dict( + { + "segments": "not_a_list", + } + ) assert config.segments == [] def test_output_size(self): """输出尺寸.""" - config = ConcatConfig.from_config_dict({ - "segments": [{"video_path": "/a.mp4"}], - "output_width": 1920, - "output_height": 1080, - }) + config = ConcatConfig.from_config_dict( + { + "segments": [{"video_path": "/a.mp4"}], + "output_width": 1920, + "output_height": 1080, + } + ) assert config.output_width == 1920 assert config.output_height == 1080 def test_negative_output_size_clamped(self): """负输出尺寸钳制到0.""" - config = ConcatConfig.from_config_dict({ - "segments": [{"video_path": "/a.mp4"}], - "output_width": -100, - "output_height": -50, - }) + config = ConcatConfig.from_config_dict( + { + "segments": [{"video_path": "/a.mp4"}], + "output_width": -100, + "output_height": -50, + } + ) assert config.output_width == 0 assert config.output_height == 0 def test_invalid_output_size_falls_back(self): """无效输出尺寸回退.""" - config = ConcatConfig.from_config_dict({ - "segments": [{"video_path": "/a.mp4"}], - "output_width": "wide", - "output_fps": "sixty", - }) + config = ConcatConfig.from_config_dict( + { + "segments": [{"video_path": "/a.mp4"}], + "output_width": "wide", + "output_fps": "sixty", + } + ) assert config.output_width == 0 assert config.output_fps == 0.0 def test_output_fps(self): """输出帧率.""" - config = ConcatConfig.from_config_dict({ - "segments": [{"video_path": "/a.mp4"}], - "output_fps": 60.0, - }) + config = ConcatConfig.from_config_dict( + { + "segments": [{"video_path": "/a.mp4"}], + "output_fps": 60.0, + } + ) assert config.output_fps == 60.0 def test_force_reencode(self): """强制重新编码.""" - config = ConcatConfig.from_config_dict({ - "segments": [{"video_path": "/a.mp4"}], - "force_reencode": True, - }) + config = ConcatConfig.from_config_dict( + { + "segments": [{"video_path": "/a.mp4"}], + "force_reencode": True, + } + ) assert config.force_reencode is True def test_transition_config(self): """转场配置.""" - config = ConcatConfig.from_config_dict({ - "segments": [{"video_path": "/a.mp4"}, {"video_path": "/b.mp4"}], - "transition": "crossfade", - "transition_duration": 1.0, - }) + config = ConcatConfig.from_config_dict( + { + "segments": [{"video_path": "/a.mp4"}, {"video_path": "/b.mp4"}], + "transition": "crossfade", + "transition_duration": 1.0, + } + ) assert config.transition == "crossfade" assert config.transition_duration == 1.0 @@ -238,17 +273,21 @@ class TestHasEffect: def test_one_segment_no_effect(self): """单片段无效果(拼接至少需要2段).""" - config = ConcatConfig(segments=[ - ConcatSegment(video_path="/a.mp4"), - ]) + config = ConcatConfig( + segments=[ + ConcatSegment(video_path="/a.mp4"), + ] + ) assert config.has_effect is False def test_two_segments_has_effect(self): """两段及以上有效果.""" - config = ConcatConfig(segments=[ - ConcatSegment(video_path="/a.mp4"), - ConcatSegment(video_path="/b.mp4"), - ]) + config = ConcatConfig( + segments=[ + ConcatSegment(video_path="/a.mp4"), + ConcatSegment(video_path="/b.mp4"), + ] + ) assert config.has_effect is True @@ -262,9 +301,11 @@ class TestTotalSegments: def test_three_segments(self): """三个片段.""" - config = ConcatConfig(segments=[ - ConcatSegment(video_path="/a.mp4"), - ConcatSegment(video_path="/b.mp4"), - ConcatSegment(video_path="/c.mp4"), - ]) + config = ConcatConfig( + segments=[ + ConcatSegment(video_path="/a.mp4"), + ConcatSegment(video_path="/b.mp4"), + ConcatSegment(video_path="/c.mp4"), + ] + ) assert config.total_segments == 3 diff --git a/tests/unit/test_ffmpeg_pure_utils.py b/tests/unit/test_ffmpeg_pure_utils.py index 825793ccd..9cd15b17c 100755 --- a/tests/unit/test_ffmpeg_pure_utils.py +++ b/tests/unit/test_ffmpeg_pure_utils.py @@ -3,12 +3,11 @@ from __future__ import annotations import pytest - from video_processing.ffmpeg_utils import ( XFADE_TRANSITION_MAP, + build_xfade_filter_chain, chain_filters, resolve_xfade_transition, - build_xfade_filter_chain, ) @@ -97,9 +96,7 @@ class TestBuildXfadeFilterChain: def test_single_clip(self): """1个片段→直接copy,总时长等于片段时长.""" - filter_str, total_dur = build_xfade_filter_chain( - [10.0], ["v0"], [], output_label="outv" - ) + filter_str, total_dur = build_xfade_filter_chain([10.0], ["v0"], [], output_label="outv") assert "[v0]copy[outv]" in filter_str assert total_dur == pytest.approx(10.0) diff --git a/tests/unit/test_intro_outro_engine.py b/tests/unit/test_intro_outro_engine.py index 2ad8c6637..21669501e 100755 --- a/tests/unit/test_intro_outro_engine.py +++ b/tests/unit/test_intro_outro_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.intro_outro_engine import IntroOutroConfig diff --git a/tests/unit/test_module_registry.py b/tests/unit/test_module_registry.py index 099851c21..41ccced6e 100755 --- a/tests/unit/test_module_registry.py +++ b/tests/unit/test_module_registry.py @@ -229,10 +229,12 @@ class TestModuleRegistryCapabilities: def test_has_capability_true(self): """检查已存在的能力.""" registry = ModuleRegistry() - registry.register(Module( - name="ai_mod", - capabilities=[ModuleCapability(name="generate_voice")], - )) + registry.register( + Module( + name="ai_mod", + capabilities=[ModuleCapability(name="generate_voice")], + ) + ) assert registry.has_capability("generate_voice") is True def test_has_capability_false(self): @@ -270,10 +272,12 @@ class TestModuleRegistryCapabilities: def test_get_quota_rules_empty(self): """没有配额规则时返回空列表.""" registry = ModuleRegistry() - registry.register(Module( - name="m1", - capabilities=[ModuleCapability(name="do_something")], - )) + registry.register( + Module( + name="m1", + capabilities=[ModuleCapability(name="do_something")], + ) + ) rules = registry.get_quota_rules("do_something") assert rules == [] @@ -281,10 +285,12 @@ class TestModuleRegistryCapabilities: """获取配额规则.""" registry = ModuleRegistry() rules = [QuotaRule("credits", 2.0)] - registry.register(Module( - name="m1", - capabilities=[ModuleCapability(name="do_something", quota_rules=rules)], - )) + registry.register( + Module( + name="m1", + capabilities=[ModuleCapability(name="do_something", quota_rules=rules)], + ) + ) result = registry.get_quota_rules("do_something") assert len(result) == 1 assert result[0].dimension == "credits" @@ -293,17 +299,21 @@ class TestModuleRegistryCapabilities: def test_get_active_capabilities(self): """获取所有已激活模块的能力.""" registry = ModuleRegistry() - registry.register(Module( - name="mod_a", - capabilities=[ - ModuleCapability(name="cap_a1"), - ModuleCapability(name="cap_a2"), - ], - )) - registry.register(Module( - name="mod_b", - capabilities=[ModuleCapability(name="cap_b1")], - )) + registry.register( + Module( + name="mod_a", + capabilities=[ + ModuleCapability(name="cap_a1"), + ModuleCapability(name="cap_a2"), + ], + ) + ) + registry.register( + Module( + name="mod_b", + capabilities=[ModuleCapability(name="cap_b1")], + ) + ) result = registry.get_active_capabilities() assert "mod_a" in result assert "mod_b" in result diff --git a/tests/unit/test_multi_track_mixer.py b/tests/unit/test_multi_track_mixer.py index a6ac87e6c..b3c838901 100755 --- a/tests/unit/test_multi_track_mixer.py +++ b/tests/unit/test_multi_track_mixer.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.multi_track_mixer import ( DEFAULT_VOLUMES, MAX_AUDIO_TRACKS, @@ -64,135 +63,163 @@ class TestAudioTrackFromDict: def test_basic_parsing(self): """基本解析.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "track_type": "bgm", - "audio_path": "/bgm.mp3", - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "track_type": "bgm", + "audio_path": "/bgm.mp3", + } + ) assert track.track_id == "t1" assert track.track_type == "bgm" assert track.audio_path == "/bgm.mp3" def test_default_volume_by_type_bgm(self): """bgm默认音量0.3.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "track_type": "bgm", - "audio_path": "/a.mp3", - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "track_type": "bgm", + "audio_path": "/a.mp3", + } + ) assert track.volume == 0.3 def test_default_volume_by_type_sfx(self): """sfx默认音量0.7.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "track_type": "sfx", - "audio_path": "/a.mp3", - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "track_type": "sfx", + "audio_path": "/a.mp3", + } + ) assert track.volume == 0.7 def test_default_volume_unknown_type(self): """未知类型默认音量1.0.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "track_type": "unknown_type", - "audio_path": "/a.mp3", - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "track_type": "unknown_type", + "audio_path": "/a.mp3", + } + ) assert track.volume == 1.0 def test_custom_volume(self): """自定义音量.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "track_type": "bgm", - "audio_path": "/a.mp3", - "volume": 0.5, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "track_type": "bgm", + "audio_path": "/a.mp3", + "volume": 0.5, + } + ) assert track.volume == 0.5 def test_volume_clamped_high(self): """音量上限钳制.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "track_type": "bgm", - "audio_path": "/a.mp3", - "volume": 3.0, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "track_type": "bgm", + "audio_path": "/a.mp3", + "volume": 3.0, + } + ) assert track.volume == 2.0 def test_volume_clamped_low(self): """音量下限钳制.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "audio_path": "/a.mp3", - "volume": -1.0, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "audio_path": "/a.mp3", + "volume": -1.0, + } + ) assert track.volume == 0.0 def test_volume_invalid_falls_back(self): """无效音量回退到类型默认值.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "track_type": "bgm", - "audio_path": "/a.mp3", - "volume": "not_a_number", - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "track_type": "bgm", + "audio_path": "/a.mp3", + "volume": "not_a_number", + } + ) assert track.volume == 0.3 def test_fade_in(self): """淡入时长.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "audio_path": "/a.mp3", - "fade_in": 2.5, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "audio_path": "/a.mp3", + "fade_in": 2.5, + } + ) assert track.fade_in == 2.5 def test_fade_negative_clamped(self): """负淡入钳制到0.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "audio_path": "/a.mp3", - "fade_in": -1.0, - "fade_out": -2.0, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "audio_path": "/a.mp3", + "fade_in": -1.0, + "fade_out": -2.0, + } + ) assert track.fade_in == 0.0 assert track.fade_out == 0.0 def test_start_time(self): """开始时间.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "audio_path": "/a.mp3", - "start_time": 5.5, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "audio_path": "/a.mp3", + "start_time": 5.5, + } + ) assert track.start_time == 5.5 def test_start_time_negative_clamped(self): """负开始时间钳制到0.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "audio_path": "/a.mp3", - "start_time": -3.0, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "audio_path": "/a.mp3", + "start_time": -3.0, + } + ) assert track.start_time == 0.0 def test_disabled_track(self): """禁用轨道.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "audio_path": "/a.mp3", - "enabled": False, - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "audio_path": "/a.mp3", + "enabled": False, + } + ) assert track.enabled is False def test_invalid_fade_in_falls_back(self): """无效淡入值回退到0.""" - track = AudioTrack.from_dict({ - "track_id": "t1", - "audio_path": "/a.mp3", - "fade_in": "fast", - }) + track = AudioTrack.from_dict( + { + "track_id": "t1", + "audio_path": "/a.mp3", + "fade_in": "fast", + } + ) assert track.fade_in == 0.0 @@ -224,89 +251,107 @@ class TestMultiTrackMixConfigFromConfigDict: def test_single_track(self): """单轨道.""" - config = MultiTrackMixConfig.from_config_dict({ - "tracks": [ - { - "track_id": "bgm1", - "track_type": "bgm", - "audio_path": "/bgm.mp3", - }, - ], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "tracks": [ + { + "track_id": "bgm1", + "track_type": "bgm", + "audio_path": "/bgm.mp3", + }, + ], + } + ) assert len(config.tracks) == 1 assert config.tracks[0].track_id == "bgm1" def test_multiple_tracks(self): """多轨道.""" - config = MultiTrackMixConfig.from_config_dict({ - "tracks": [ - {"track_id": "t1", "track_type": "bgm", "audio_path": "/a.mp3"}, - {"track_id": "t2", "track_type": "sfx", "audio_path": "/b.mp3"}, - ], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "tracks": [ + {"track_id": "t1", "track_type": "bgm", "audio_path": "/a.mp3"}, + {"track_id": "t2", "track_type": "sfx", "audio_path": "/b.mp3"}, + ], + } + ) assert len(config.tracks) == 2 def test_skips_disabled_tracks(self): """跳过禁用轨道.""" - config = MultiTrackMixConfig.from_config_dict({ - "tracks": [ - {"track_id": "t1", "audio_path": "/a.mp3", "enabled": True}, - {"track_id": "t2", "audio_path": "/b.mp3", "enabled": False}, - ], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "tracks": [ + {"track_id": "t1", "audio_path": "/a.mp3", "enabled": True}, + {"track_id": "t2", "audio_path": "/b.mp3", "enabled": False}, + ], + } + ) assert len(config.tracks) == 1 assert config.tracks[0].track_id == "t1" def test_skips_no_audio_path(self): """跳过无audio_path的轨道.""" - config = MultiTrackMixConfig.from_config_dict({ - "tracks": [ - {"track_id": "t1", "audio_path": "/a.mp3"}, - {"track_id": "t2", "audio_path": ""}, - {"track_id": "t3"}, - ], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "tracks": [ + {"track_id": "t1", "audio_path": "/a.mp3"}, + {"track_id": "t2", "audio_path": ""}, + {"track_id": "t3"}, + ], + } + ) assert len(config.tracks) == 1 def test_master_volume(self): """主音量.""" - config = MultiTrackMixConfig.from_config_dict({ - "master_volume": 0.8, - "tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "master_volume": 0.8, + "tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}], + } + ) assert config.master_volume == 0.8 def test_master_volume_clamped(self): """主音量边界钳制.""" - config = MultiTrackMixConfig.from_config_dict({ - "master_volume": 5.0, - "tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "master_volume": 5.0, + "tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}], + } + ) assert config.master_volume == 2.0 def test_normalize_disabled(self): """禁用归一化.""" - config = MultiTrackMixConfig.from_config_dict({ - "normalize": False, - "tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "normalize": False, + "tracks": [{"track_id": "t1", "audio_path": "/a.mp3"}], + } + ) assert config.normalize is False def test_tracks_not_list_ignored(self): """tracks不是列表时忽略.""" - config = MultiTrackMixConfig.from_config_dict({ - "tracks": "not_a_list", - }) + config = MultiTrackMixConfig.from_config_dict( + { + "tracks": "not_a_list", + } + ) assert config.tracks == [] def test_non_dict_track_skipped(self): """非dict轨道跳过.""" - config = MultiTrackMixConfig.from_config_dict({ - "tracks": [ - {"track_id": "t1", "audio_path": "/a.mp3"}, - "not_a_dict", - ], - }) + config = MultiTrackMixConfig.from_config_dict( + { + "tracks": [ + {"track_id": "t1", "audio_path": "/a.mp3"}, + "not_a_dict", + ], + } + ) assert len(config.tracks) == 1 @@ -320,14 +365,18 @@ class TestHasEffect: def test_with_tracks_has_effect(self): """有轨道有效果.""" - config = MultiTrackMixConfig(tracks=[ - AudioTrack(track_id="t1", track_type="bgm", audio_path="/a.mp3"), - ]) + config = MultiTrackMixConfig( + tracks=[ + AudioTrack(track_id="t1", track_type="bgm", audio_path="/a.mp3"), + ] + ) assert config.has_effect is True def test_disabled_tracks_no_effect(self): """所有轨道都禁用无效果.""" - config = MultiTrackMixConfig(tracks=[ - AudioTrack(track_id="t1", track_type="bgm", audio_path="/a.mp3", enabled=False), - ]) + config = MultiTrackMixConfig( + tracks=[ + AudioTrack(track_id="t1", track_type="bgm", audio_path="/a.mp3", enabled=False), + ] + ) assert config.has_effect is False diff --git a/tests/unit/test_noise_reduction_engine.py b/tests/unit/test_noise_reduction_engine.py index f7a283eb3..fee5a71b5 100755 --- a/tests/unit/test_noise_reduction_engine.py +++ b/tests/unit/test_noise_reduction_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.noise_reduction_engine import ( NoiseReductionConfig, NoiseReductionLevel, @@ -96,64 +95,78 @@ class TestNoiseReductionConfigFromDict: def test_noise_floor_parsed(self): """噪音阈值解析.""" - config = NoiseReductionConfig.from_dict({ - "enabled": True, - "level": "custom", - "noise_floor": -30.0, - }) + config = NoiseReductionConfig.from_dict( + { + "enabled": True, + "level": "custom", + "noise_floor": -30.0, + } + ) assert config.noise_floor == -30.0 def test_noise_floor_clamped_min(self): """噪音阈值下限钳制 (-60).""" - config = NoiseReductionConfig.from_dict({ - "enabled": True, - "level": "custom", - "noise_floor": -100.0, - }) + config = NoiseReductionConfig.from_dict( + { + "enabled": True, + "level": "custom", + "noise_floor": -100.0, + } + ) assert config.noise_floor == -60.0 def test_noise_floor_clamped_max(self): """噪音阈值上限钳制 (-5).""" - config = NoiseReductionConfig.from_dict({ - "enabled": True, - "level": "custom", - "noise_floor": 0.0, - }) + config = NoiseReductionConfig.from_dict( + { + "enabled": True, + "level": "custom", + "noise_floor": 0.0, + } + ) assert config.noise_floor == -5.0 def test_noise_floor_boundary_low(self): """噪音阈值边界值 -60.""" - config = NoiseReductionConfig.from_dict({ - "enabled": True, - "level": "custom", - "noise_floor": -60.0, - }) + config = NoiseReductionConfig.from_dict( + { + "enabled": True, + "level": "custom", + "noise_floor": -60.0, + } + ) assert config.noise_floor == -60.0 def test_noise_floor_boundary_high(self): """噪音阈值边界值 -5.""" - config = NoiseReductionConfig.from_dict({ - "enabled": True, - "level": "custom", - "noise_floor": -5.0, - }) + config = NoiseReductionConfig.from_dict( + { + "enabled": True, + "level": "custom", + "noise_floor": -5.0, + } + ) assert config.noise_floor == -5.0 def test_invalid_noise_floor_falls_back(self): """无效噪音阈值 fallback 到默认值.""" - config = NoiseReductionConfig.from_dict({ - "enabled": True, - "level": "custom", - "noise_floor": "not_a_number", - }) + config = NoiseReductionConfig.from_dict( + { + "enabled": True, + "level": "custom", + "noise_floor": "not_a_number", + } + ) assert config.noise_floor == -25.0 def test_voice_enhance_enabled(self): """人声增强启用.""" - config = NoiseReductionConfig.from_dict({ - "enabled": True, - "voice_enhance": True, - }) + config = NoiseReductionConfig.from_dict( + { + "enabled": True, + "voice_enhance": True, + } + ) assert config.voice_enhance is True def test_voice_enhance_disabled_default(self): diff --git a/tests/unit/test_oss_helpers_pure.py b/tests/unit/test_oss_helpers_pure.py index 349612121..59cc96e89 100755 --- a/tests/unit/test_oss_helpers_pure.py +++ b/tests/unit/test_oss_helpers_pure.py @@ -8,7 +8,6 @@ from pathlib import Path from unittest.mock import patch import pytest - from video_processing.oss_helpers import normalize_storage_key, resolve_asset_path @@ -21,23 +20,17 @@ class TestNormalizeStorageKey: def test_https_url_extracts_path(self): """HTTPS URL提取path部分.""" - result = normalize_storage_key( - "https://bucket.oss-cn-hangzhou.aliyuncs.com/path/to/file.mp4" - ) + result = normalize_storage_key("https://bucket.oss-cn-hangzhou.aliyuncs.com/path/to/file.mp4") assert result == "path/to/file.mp4" def test_http_url_extracts_path(self): """HTTP URL提取path部分.""" - result = normalize_storage_key( - "http://example.com/assets/video.mp4" - ) + result = normalize_storage_key("http://example.com/assets/video.mp4") assert result == "assets/video.mp4" def test_url_with_query_params(self): """带query参数的URL只取path.""" - result = normalize_storage_key( - "https://bucket.oss-cn-hangzhou.aliyuncs.com/file.mp4?token=abc&expires=123" - ) + result = normalize_storage_key("https://bucket.oss-cn-hangzhou.aliyuncs.com/file.mp4?token=abc&expires=123") assert result == "file.mp4" def test_leading_slash_stripped(self): diff --git a/tests/unit/test_pip_engine.py b/tests/unit/test_pip_engine.py index 8ba0c2033..06f26a3de 100755 --- a/tests/unit/test_pip_engine.py +++ b/tests/unit/test_pip_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.pip_engine import PiPConfig, PiPLayerConfig @@ -178,12 +177,14 @@ class TestPiPConfigFromDict: def test_single_layer(self): """单个图层.""" - config = PiPConfig.from_dict({ - "enabled": True, - "layers": [ - {"source": "asset_001", "position": "top_left"}, - ], - }) + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": "asset_001", "position": "top_left"}, + ], + } + ) assert config.enabled is True assert len(config.layers) == 1 assert config.layers[0].source == "asset_001" @@ -191,14 +192,16 @@ class TestPiPConfigFromDict: def test_multiple_layers_sorted_by_z_index(self): """多个图层按z_index排序.""" - config = PiPConfig.from_dict({ - "enabled": True, - "layers": [ - {"source": "a", "z_index": 3}, - {"source": "b", "z_index": 1}, - {"source": "c", "z_index": 2}, - ], - }) + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": "a", "z_index": 3}, + {"source": "b", "z_index": 1}, + {"source": "c", "z_index": 2}, + ], + } + ) assert len(config.layers) == 3 assert config.layers[0].z_index == 1 assert config.layers[1].z_index == 2 @@ -206,48 +209,54 @@ class TestPiPConfigFromDict: def test_invalid_layer_skipped(self): """无效图层跳过.""" - config = PiPConfig.from_dict({ - "enabled": True, - "layers": [ - {"source": "valid_asset"}, - {"source": ""}, # 无效,空source - ], - }) + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": "valid_asset"}, + {"source": ""}, # 无效,空source + ], + } + ) assert len(config.layers) == 1 assert config.layers[0].source == "valid_asset" def test_all_invalid_layers_disabled(self): """全部无效则disabled.""" - config = PiPConfig.from_dict({ - "enabled": True, - "layers": [ - {"source": ""}, - {"source": ""}, - ], - }) + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + {"source": ""}, + {"source": ""}, + ], + } + ) assert config.enabled is False assert config.layers == [] def test_layer_full_config(self): """完整图层配置.""" - config = PiPConfig.from_dict({ - "enabled": True, - "layers": [ - { - "source": "https://example.com/video.mp4", - "source_type": "url", - "position": "bottom_right", - "width": "30%", - "opacity": 0.8, - "corner_radius": 10, - "border_width": 2, - "border_color": "red", - "start_time": 5.0, - "duration": 10.0, - "z_index": 5, - }, - ], - }) + config = PiPConfig.from_dict( + { + "enabled": True, + "layers": [ + { + "source": "https://example.com/video.mp4", + "source_type": "url", + "position": "bottom_right", + "width": "30%", + "opacity": 0.8, + "corner_radius": 10, + "border_width": 2, + "border_color": "red", + "start_time": 5.0, + "duration": 10.0, + "z_index": 5, + }, + ], + } + ) assert len(config.layers) == 1 layer = config.layers[0] assert layer.source == "https://example.com/video.mp4" diff --git a/tests/unit/test_render_adapter_pure.py b/tests/unit/test_render_adapter_pure.py index 921f71d65..0812063de 100755 --- a/tests/unit/test_render_adapter_pure.py +++ b/tests/unit/test_render_adapter_pure.py @@ -5,7 +5,6 @@ from __future__ import annotations from pathlib import Path import pytest - from video_processing.render_adapter import ( DEFAULT_OUTPUT_HEIGHT, DEFAULT_OUTPUT_WIDTH, diff --git a/tests/unit/test_render_audio_pure.py b/tests/unit/test_render_audio_pure.py index 02f39142d..bedd77eb9 100755 --- a/tests/unit/test_render_audio_pure.py +++ b/tests/unit/test_render_audio_pure.py @@ -7,7 +7,6 @@ from pathlib import Path from unittest.mock import patch import pytest - from video_processing.render_audio import ( RenderContext, clip_effective_duration, diff --git a/tests/unit/test_speed_engine.py b/tests/unit/test_speed_engine.py index bcb0128e4..c5d681cff 100755 --- a/tests/unit/test_speed_engine.py +++ b/tests/unit/test_speed_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.speed_engine import ( MAX_SPEED, MIN_SPEED, @@ -11,7 +10,6 @@ from video_processing.speed_engine import ( SpeedEngine, ) - # ── 常量测试 ────────────────────────────────────────────────── diff --git a/tests/unit/test_sticker_engine.py b/tests/unit/test_sticker_engine.py index a2023f33c..d95fe07ca 100755 --- a/tests/unit/test_sticker_engine.py +++ b/tests/unit/test_sticker_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.sticker_engine import ( ImageStickerConfig, TextStickerConfig, @@ -95,21 +94,25 @@ class TestParseStickersFromConfig: def test_single_sticker(self): """单个贴纸.""" - result = parse_stickers_from_config({ - "stickers": [{"type": "text", "text": "hello"}], - }) + result = parse_stickers_from_config( + { + "stickers": [{"type": "text", "text": "hello"}], + } + ) assert len(result) == 1 assert result[0]["text"] == "hello" def test_multiple_stickers(self): """多个贴纸.""" - result = parse_stickers_from_config({ - "stickers": [ - {"type": "text", "text": "a"}, - {"type": "image", "image_url": "/b.png"}, - {"type": "text", "text": "c"}, - ], - }) + result = parse_stickers_from_config( + { + "stickers": [ + {"type": "text", "text": "a"}, + {"type": "image", "image_url": "/b.png"}, + {"type": "text", "text": "c"}, + ], + } + ) assert len(result) == 3 def test_returns_raw_dicts(self): diff --git a/tests/unit/test_subtitle_render_engine.py b/tests/unit/test_subtitle_render_engine.py index 113e12154..0ad5cff84 100755 --- a/tests/unit/test_subtitle_render_engine.py +++ b/tests/unit/test_subtitle_render_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.subtitle_render_engine import ( SubtitleStyle, _escape_ass_text, @@ -14,7 +13,6 @@ from video_processing.subtitle_render_engine import ( _wrap_text, ) - # ── 颜色转换测试 ────────────────────────────────────────────── @@ -269,10 +267,12 @@ class TestSubtitleStyleFromDict: def test_background_opacity_clamped(self): """背景透明度钳制.""" - style = SubtitleStyle.from_dict({ - "background_enabled": True, - "background_opacity": 2.0, - }) + style = SubtitleStyle.from_dict( + { + "background_enabled": True, + "background_opacity": 2.0, + } + ) assert style.background_opacity == 1.0 def test_invalid_position_falls_back(self): diff --git a/tests/unit/test_templates_editor_utils.py b/tests/unit/test_templates_editor_utils.py index c28adc564..525a545fb 100755 --- a/tests/unit/test_templates_editor_utils.py +++ b/tests/unit/test_templates_editor_utils.py @@ -5,7 +5,6 @@ from __future__ import annotations from dataclasses import dataclass import pytest - from app.api.routes.templates_editor._utils import ( _clip_type_to_scene_label, _clip_value, diff --git a/tests/unit/test_thumbnail_generator.py b/tests/unit/test_thumbnail_generator.py index 8517e11ef..c2f9686ec 100755 --- a/tests/unit/test_thumbnail_generator.py +++ b/tests/unit/test_thumbnail_generator.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.thumbnail_generator import _format_seek_time diff --git a/tests/unit/test_transition_engine.py b/tests/unit/test_transition_engine.py index ed8213b8d..4aec1d4d5 100755 --- a/tests/unit/test_transition_engine.py +++ b/tests/unit/test_transition_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.transition_engine import ( CUT_TRANSITION, DEFAULT_TRANSITION_DURATION, diff --git a/tests/unit/test_trim_engine.py b/tests/unit/test_trim_engine.py index 55e06ca06..4601f5b22 100755 --- a/tests/unit/test_trim_engine.py +++ b/tests/unit/test_trim_engine.py @@ -3,7 +3,6 @@ from __future__ import annotations import pytest - from video_processing.trim_engine import MIN_TRIM_DURATION, TrimConfig, TrimSegment @@ -20,11 +19,16 @@ class TestTrimConfigFromDict: def test_all_zero_returns_none(self): """全零返回None.""" - assert TrimConfig.from_dict({ - "start_time": 0, - "end_time": 0, - "duration": 0, - }) is None + assert ( + TrimConfig.from_dict( + { + "start_time": 0, + "end_time": 0, + "duration": 0, + } + ) + is None + ) def test_start_only(self): """只有start_time有效.""" @@ -64,10 +68,12 @@ class TestTrimConfigFromDict: def test_string_values_converted(self): """字符串值会被转换.""" - config = TrimConfig.from_dict({ - "start_time": "5.0", - "duration": "10.0", - }) + config = TrimConfig.from_dict( + { + "start_time": "5.0", + "duration": "10.0", + } + ) assert config is not None assert config.start_time == 5.0 assert config.duration == 10.0 @@ -235,11 +241,14 @@ class TestTrimSegment: def test_from_dict_basic(self): """基本解析.""" - seg = TrimSegment.from_dict({ - "start_time": 5.0, - "duration": 10.0, - "segment_id": "seg1", - }, default_order=0) + seg = TrimSegment.from_dict( + { + "start_time": 5.0, + "duration": 10.0, + "segment_id": "seg1", + }, + default_order=0, + ) assert seg.segment_id == "seg1" assert seg.trim.start_time == 5.0 assert seg.trim.duration == 10.0 @@ -247,11 +256,13 @@ class TestTrimSegment: def test_from_dict_with_order(self): """带order的解析.""" - seg = TrimSegment.from_dict({ - "start_time": 1.0, - "end_time": 4.0, - "order": 2, - }) + seg = TrimSegment.from_dict( + { + "start_time": 1.0, + "end_time": 4.0, + "order": 2, + } + ) assert seg.order == 2 assert seg.trim.start_time == 1.0 assert seg.trim.end_time == 4.0 @@ -264,8 +275,11 @@ class TestTrimSegment: def test_from_dict_empty_string_segment_id(self): """空字符串segment_id走默认.""" - seg = TrimSegment.from_dict({ - "segment_id": "", - "duration": 5.0, - }, default_order=5) + seg = TrimSegment.from_dict( + { + "segment_id": "", + "duration": 5.0, + }, + default_order=5, + ) assert seg.segment_id == "seg_5" diff --git a/tests/unit/test_unified_render_pure.py b/tests/unit/test_unified_render_pure.py index d5615da7b..e2b27f143 100755 --- a/tests/unit/test_unified_render_pure.py +++ b/tests/unit/test_unified_render_pure.py @@ -5,12 +5,11 @@ from __future__ import annotations from pathlib import Path import pytest - from video_processing.unified_render_service import ( - RenderLayer, - ResolvedClip, _LAYER_Z_INDEX, _PIP_SCALE, + RenderLayer, + ResolvedClip, _resolve_layer_role, ) @@ -159,9 +158,11 @@ class TestRenderLayer: def test_with_clips(self): """带片段的图层.""" clip = ResolvedClip( - clip_id="c1", asset_id="a1", + clip_id="c1", + asset_id="a1", local_path=Path("/tmp/t.mp4"), - clip_type="main", order=0, + clip_type="main", + order=0, ) layer = RenderLayer(role="overlay", clips=[clip], z_index=1) assert len(layer.clips) == 1 diff --git a/tests/unit/test_watermark_engine.py b/tests/unit/test_watermark_engine.py index 231c4185f..3d10d5dad 100755 --- a/tests/unit/test_watermark_engine.py +++ b/tests/unit/test_watermark_engine.py @@ -3,14 +3,12 @@ from __future__ import annotations import pytest - from video_processing.watermark_engine import ( WATERMARK_POSITIONS, WatermarkConfig, WatermarkEngine, ) - # ── WatermarkConfig 测试 ────────────────────────────────────────── @@ -52,11 +50,13 @@ class TestWatermarkConfigFromDict: def test_text_mode_basic(self): """文字水印基本配置.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "text", - "text": "测试水印", - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "text", + "text": "测试水印", + } + ) assert config is not None assert config.mode == "text" assert config.text == "测试水印" @@ -64,86 +64,102 @@ class TestWatermarkConfigFromDict: def test_text_mode_missing_text_returns_none(self): """文字水印缺少 text 返回 None.""" - result = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "text", - }) + result = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "text", + } + ) assert result is None def test_text_mode_empty_text_returns_none(self): """文字水印 text 为空返回 None.""" - result = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "text", - "text": "", - }) + result = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "text", + "text": "", + } + ) assert result is None def test_image_mode_basic(self): """图片水印基本配置.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "image", - "image_path": "/path/to/logo.png", - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "image", + "image_path": "/path/to/logo.png", + } + ) assert config is not None assert config.mode == "image" assert config.image_path == "/path/to/logo.png" def test_image_mode_missing_image_returns_none(self): """图片水印缺少 image_path 返回 None.""" - result = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "image", - }) + result = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "image", + } + ) assert result is None def test_image_mode_image_alias(self): """image 字段作为 image_path 的别名.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "image", - "image": "/path/alias.png", - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "image", + "image": "/path/alias.png", + } + ) assert config is not None assert config.image_path == "/path/alias.png" def test_invalid_position_falls_back(self): """无效位置 fallback 到 bottom_right.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "text", - "text": "test", - "position": "invalid_pos", - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "text", + "text": "test", + "position": "invalid_pos", + } + ) assert config is not None assert config.position == "bottom_right" def test_custom_position_valid(self): """自定义有效位置.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "text", - "text": "test", - "position": "top_left", - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "text", + "text": "test", + "position": "top_left", + } + ) assert config is not None assert config.position == "top_left" def test_all_text_fields_parsed(self): """文字水印所有字段正确解析.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "text", - "text": "我的水印", - "font_size": 32, - "font_color": "red", - "font_path": "/fonts/msyh.ttf", - "position": "top_center", - "opacity": 0.5, - "margin_x": 30, - "margin_y": 40, - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "text", + "text": "我的水印", + "font_size": 32, + "font_color": "red", + "font_path": "/fonts/msyh.ttf", + "position": "top_center", + "opacity": 0.5, + "margin_x": 30, + "margin_y": 40, + } + ) assert config is not None assert config.text == "我的水印" assert config.font_size == 32 @@ -156,16 +172,18 @@ class TestWatermarkConfigFromDict: def test_all_image_fields_parsed(self): """图片水印所有字段正确解析.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "image", - "image_path": "/img/logo.png", - "scale": 0.3, - "opacity": 0.9, - "position": "bottom_left", - "margin_x": 10, - "margin_y": 15, - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "image", + "image_path": "/img/logo.png", + "scale": 0.3, + "opacity": 0.9, + "position": "bottom_left", + "margin_x": 10, + "margin_y": 15, + } + ) assert config is not None assert config.image_path == "/img/logo.png" assert config.scale == 0.3 @@ -174,23 +192,27 @@ class TestWatermarkConfigFromDict: def test_scroll_config_parsed(self): """滚动水印配置解析.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "mode": "text", - "text": "滚动水印", - "scroll": True, - "scroll_speed": 80, - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "mode": "text", + "text": "滚动水印", + "scroll": True, + "scroll_speed": 80, + } + ) assert config is not None assert config.scroll is True assert config.scroll_speed == 80 def test_default_mode_is_text(self): """不传 mode 默认为 text.""" - config = WatermarkConfig.from_dict({ - "enabled": True, - "text": "默认模式", - }) + config = WatermarkConfig.from_dict( + { + "enabled": True, + "text": "默认模式", + } + ) assert config is not None assert config.mode == "text" @@ -325,95 +347,71 @@ class TestCalcPosition: def test_top_left(self): """左上角.""" - x, y = WatermarkEngine.calc_position( - "top_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("top_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert (x, y) == (20, 20) def test_top_center(self): """中上.""" - x, y = WatermarkEngine.calc_position( - "top_center", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("top_center", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == (1920 - 200) // 2 assert y == 20 def test_top_right(self): """右上角.""" - x, y = WatermarkEngine.calc_position( - "top_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("top_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == 1920 - 200 - 20 assert y == 20 def test_center_left(self): """左中.""" - x, y = WatermarkEngine.calc_position( - "center_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("center_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == 20 assert y == (1080 - 100) // 2 def test_center(self): """中心.""" - x, y = WatermarkEngine.calc_position( - "center", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("center", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == (1920 - 200) // 2 assert y == (1080 - 100) // 2 def test_center_right(self): """右中.""" - x, y = WatermarkEngine.calc_position( - "center_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("center_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == 1920 - 200 - 20 assert y == (1080 - 100) // 2 def test_bottom_left(self): """左下角.""" - x, y = WatermarkEngine.calc_position( - "bottom_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("bottom_left", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == 20 assert y == 1080 - 100 - 20 def test_bottom_center(self): """中下.""" - x, y = WatermarkEngine.calc_position( - "bottom_center", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("bottom_center", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == (1920 - 200) // 2 assert y == 1080 - 100 - 20 def test_bottom_right(self): """右下角.""" - x, y = WatermarkEngine.calc_position( - "bottom_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("bottom_right", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == 1920 - 200 - 20 assert y == 1080 - 100 - 20 def test_unknown_position_defaults_bottom_right(self): """未知位置默认右下角.""" - x, y = WatermarkEngine.calc_position( - "unknown", self.W, self.H, self.WW, self.WH, self.MX, self.MY - ) + x, y = WatermarkEngine.calc_position("unknown", self.W, self.H, self.WW, self.WH, self.MX, self.MY) assert x == 1920 - 200 - 20 assert y == 1080 - 100 - 20 def test_zero_margin(self): """零边距.""" - x, y = WatermarkEngine.calc_position( - "top_left", 1000, 500, 100, 50, 0, 0 - ) + x, y = WatermarkEngine.calc_position("top_left", 1000, 500, 100, 50, 0, 0) assert (x, y) == (0, 0) def test_small_output(self): """小尺寸输出.""" - x, y = WatermarkEngine.calc_position( - "bottom_right", 320, 240, 50, 30, 5, 5 - ) + x, y = WatermarkEngine.calc_position("bottom_right", 320, 240, 50, 30, 5, 5) assert x == 320 - 50 - 5 assert y == 240 - 30 - 5 @@ -446,8 +444,14 @@ class TestWatermarkPositions: def test_all_position_keys_valid(self): """所有位置键名正确.""" expected = { - "top_left", "top_center", "top_right", - "center_left", "center", "center_right", - "bottom_left", "bottom_center", "bottom_right", + "top_left", + "top_center", + "top_right", + "center_left", + "center", + "center_right", + "bottom_left", + "bottom_center", + "bottom_right", } assert set(WATERMARK_POSITIONS.keys()) == expected