"""TTS 配音配置模型单元测试.""" from __future__ import annotations import pytest from packages.domain.tts_config import TtsConfig # ── 默认值测试 ──────────────────────────────────────────────────────────────── class TestTtsConfigDefaults: """TtsConfig 默认值测试.""" def test_default_enabled_false(self): """默认禁用配音.""" cfg = TtsConfig() assert cfg.enabled is False def test_default_voice_id_empty(self): """默认空音色ID.""" cfg = TtsConfig() assert cfg.voice_id == "" def test_default_speed(self): """默认语速1.0.""" cfg = TtsConfig() assert cfg.speed == 1.0 def test_default_pitch(self): """默认语调0.""" cfg = TtsConfig() assert cfg.pitch == 0.0 def test_default_volume(self): """默认音量0.8.""" cfg = TtsConfig() assert cfg.volume == 0.8 def test_default_text_empty(self): """默认空文本.""" cfg = TtsConfig() assert cfg.text == "" def test_default_align_mode(self): """默认整段配音对齐.""" cfg = TtsConfig() assert cfg.align_mode == "full" def test_default_overlap_mode(self): """默认替换原音.""" cfg = TtsConfig() assert cfg.overlap_mode == "replace" # ── parse - 基础场景测试 ───────────────────────────────────────────────────── class TestTtsConfigParseBasic: """TtsConfig.parse 基础场景测试.""" def test_none_data(self): """None输入返回默认配置(禁用).""" cfg = TtsConfig.parse(None) assert cfg.enabled is False def test_empty_dict(self): """空dict返回默认配置.""" cfg = TtsConfig.parse({}) assert cfg.enabled is False def test_not_dict_returns_default(self): """非dict输入返回默认.""" cfg = TtsConfig.parse("not_a_dict") assert cfg.enabled is False assert cfg.speed == 1.0 def test_disabled_returns_fast(self): """enabled为False时直接返回disabled配置.""" cfg = TtsConfig.parse({"enabled": False, "voice_id": "v1", "speed": 1.5}) assert cfg.enabled is False # 其他字段为默认值 assert cfg.voice_id == "" assert cfg.speed == 1.0 def test_enabled_basic(self): """启用配音基础配置.""" cfg = TtsConfig.parse({"enabled": True, "voice_id": "voice_001"}) assert cfg.enabled is True assert cfg.voice_id == "voice_001" def test_full_config(self): """完整配置解析.""" cfg = TtsConfig.parse( { "enabled": True, "voice_id": "v_test", "speed": 1.2, "pitch": 2.5, "volume": 0.9, "text": "大家好", "align_mode": "subtitle", "overlap_mode": "mix", } ) assert cfg.enabled is True assert cfg.voice_id == "v_test" assert cfg.speed == 1.2 assert cfg.pitch == 2.5 assert cfg.volume == 0.9 assert cfg.text == "大家好" assert cfg.align_mode == "subtitle" assert cfg.overlap_mode == "mix" # ── parse - 类型校验测试 ───────────────────────────────────────────────────── class TestTtsConfigParseTypeChecks: """TtsConfig.parse 类型校验测试.""" def test_enabled_not_bool(self): """enabled不是bool时视为False.""" cfg = TtsConfig.parse({"enabled": "true", "voice_id": "v1"}) assert cfg.enabled is False def test_enabled_int_treated_as_non_bool(self): """enabled为整数时视为非bool(Python里1是True但isinstance(1, bool)是True?).""" # Python里bool是int的子类,isinstance(True, int)为True # 反过来 isinstance(1, bool) 为 False,所以1会被当作无效值 cfg = TtsConfig.parse({"enabled": 1, "voice_id": "v1"}) assert cfg.enabled is False def test_voice_id_not_string(self): """voice_id不是字符串时回退到空.""" cfg = TtsConfig.parse({"enabled": True, "voice_id": 123}) assert cfg.voice_id == "" def test_speed_not_number(self): """speed不是数字时回退到1.0.""" cfg = TtsConfig.parse({"enabled": True, "speed": "fast"}) assert cfg.speed == 1.0 def test_speed_int_accepted(self): """整数speed也接受.""" cfg = TtsConfig.parse({"enabled": True, "speed": 2}) assert cfg.speed == 2.0 def test_pitch_not_number(self): """pitch不是数字时回退到0.""" cfg = TtsConfig.parse({"enabled": True, "pitch": "high"}) assert cfg.pitch == 0.0 def test_pitch_int_accepted(self): """整数pitch也接受.""" cfg = TtsConfig.parse({"enabled": True, "pitch": 5}) assert cfg.pitch == 5.0 def test_volume_not_number(self): """volume不是数字时回退到0.8.""" cfg = TtsConfig.parse({"enabled": True, "volume": "loud"}) assert cfg.volume == 0.8 def test_volume_int_accepted(self): """整数volume也接受.""" cfg = TtsConfig.parse({"enabled": True, "volume": 1}) assert cfg.volume == 1.0 def test_text_not_string(self): """text不是字符串时回退到空.""" cfg = TtsConfig.parse({"enabled": True, "text": 12345}) assert cfg.text == "" def test_text_empty_string(self): """空文本字符串是有效的.""" cfg = TtsConfig.parse({"enabled": True, "text": ""}) assert cfg.text == "" # ── parse - 边界钳制测试 ───────────────────────────────────────────────────── class TestTtsConfigParseClamping: """TtsConfig.parse 边界钳制测试.""" # speed 边界 def test_speed_below_min_clamped(self): """语速低于最小值钳制到0.5.""" cfg = TtsConfig.parse({"enabled": True, "speed": 0.1}) assert cfg.speed == 0.5 def test_speed_negative_clamped(self): """负语速钳制到0.5.""" cfg = TtsConfig.parse({"enabled": True, "speed": -1.0}) assert cfg.speed == 0.5 def test_speed_above_max_clamped(self): """语速高于最大值钳制到2.0.""" cfg = TtsConfig.parse({"enabled": True, "speed": 5.0}) assert cfg.speed == 2.0 def test_speed_at_min_ok(self): """刚好等于最小值正常.""" cfg = TtsConfig.parse({"enabled": True, "speed": 0.5}) assert cfg.speed == 0.5 def test_speed_at_max_ok(self): """刚好等于最大值正常.""" cfg = TtsConfig.parse({"enabled": True, "speed": 2.0}) assert cfg.speed == 2.0 def test_speed_normal_ok(self): """正常范围内语速保持不变.""" cfg = TtsConfig.parse({"enabled": True, "speed": 1.5}) assert cfg.speed == 1.5 # pitch 边界 def test_pitch_below_min_clamped(self): """语调低于最小值钳制到-12.""" cfg = TtsConfig.parse({"enabled": True, "pitch": -20}) assert cfg.pitch == -12 def test_pitch_above_max_clamped(self): """语调高于最大值钳制到12.""" cfg = TtsConfig.parse({"enabled": True, "pitch": 20}) assert cfg.pitch == 12 def test_pitch_at_min_ok(self): """刚好等于最小值正常.""" cfg = TtsConfig.parse({"enabled": True, "pitch": -12}) assert cfg.pitch == -12 def test_pitch_at_max_ok(self): """刚好等于最大值正常.""" cfg = TtsConfig.parse({"enabled": True, "pitch": 12}) assert cfg.pitch == 12 def test_pitch_normal_ok(self): """正常范围内语调保持不变.""" cfg = TtsConfig.parse({"enabled": True, "pitch": 3.5}) assert cfg.pitch == 3.5 # volume 边界 def test_volume_below_min_clamped(self): """音量低于最小值钳制到0.""" cfg = TtsConfig.parse({"enabled": True, "volume": -0.5}) assert cfg.volume == 0.0 def test_volume_above_max_clamped(self): """音量高于最大值钳制到1.""" cfg = TtsConfig.parse({"enabled": True, "volume": 2.0}) assert cfg.volume == 1.0 def test_volume_at_min_ok(self): """刚好等于最小值正常.""" cfg = TtsConfig.parse({"enabled": True, "volume": 0.0}) assert cfg.volume == 0.0 def test_volume_at_max_ok(self): """刚好等于最大值正常.""" cfg = TtsConfig.parse({"enabled": True, "volume": 1.0}) assert cfg.volume == 1.0 def test_volume_normal_ok(self): """正常范围内音量保持不变.""" cfg = TtsConfig.parse({"enabled": True, "volume": 0.6}) assert cfg.volume == 0.6 # ── parse - 枚举值校验测试 ─────────────────────────────────────────────────── class TestTtsConfigParseEnumValues: """TtsConfig.parse 枚举值校验测试.""" # align_mode def test_align_mode_subtitle(self): """subtitle对齐模式有效.""" cfg = TtsConfig.parse({"enabled": True, "align_mode": "subtitle"}) assert cfg.align_mode == "subtitle" def test_align_mode_full(self): """full对齐模式有效.""" cfg = TtsConfig.parse({"enabled": True, "align_mode": "full"}) assert cfg.align_mode == "full" def test_align_mode_invalid_fallback(self): """无效align_mode回退到full.""" cfg = TtsConfig.parse({"enabled": True, "align_mode": "word_by_word"}) assert cfg.align_mode == "full" # overlap_mode def test_overlap_mode_replace(self): """replace叠加模式有效.""" cfg = TtsConfig.parse({"enabled": True, "overlap_mode": "replace"}) assert cfg.overlap_mode == "replace" def test_overlap_mode_mix(self): """mix叠加模式有效.""" cfg = TtsConfig.parse({"enabled": True, "overlap_mode": "mix"}) assert cfg.overlap_mode == "mix" def test_overlap_mode_invalid_fallback(self): """无效overlap_mode回退到replace.""" cfg = TtsConfig.parse({"enabled": True, "overlap_mode": "duck"}) assert cfg.overlap_mode == "replace" # ── _clamp 直接测试 ────────────────────────────────────────────────────────── class TestTtsConfigClampDirect: """_clamp 方法直接调用测试.""" def test_clamp_speed_low(self): """手动构造低语速再clamp.""" cfg = TtsConfig(enabled=True, speed=0.1) cfg._clamp() assert cfg.speed == 0.5 def test_clamp_speed_high(self): """手动构造高速再clamp.""" cfg = TtsConfig(enabled=True, speed=10) cfg._clamp() assert cfg.speed == 2.0 def test_clamp_pitch_low(self): """手动构造低调再clamp.""" cfg = TtsConfig(enabled=True, pitch=-20) cfg._clamp() assert cfg.pitch == -12 def test_clamp_pitch_high(self): """手动构造高调再clamp.""" cfg = TtsConfig(enabled=True, pitch=20) cfg._clamp() assert cfg.pitch == 12 def test_clamp_volume_low(self): """手动构造低音量再clamp.""" cfg = TtsConfig(enabled=True, volume=-1) cfg._clamp() assert cfg.volume == 0.0 def test_clamp_volume_high(self): """手动构造高音量再clamp.""" cfg = TtsConfig(enabled=True, volume=2) cfg._clamp() assert cfg.volume == 1.0 def test_clamp_preserves_in_range(self): """范围内的值不变.""" cfg = TtsConfig(enabled=True, speed=1.2, pitch=3, volume=0.7) cfg._clamp() assert cfg.speed == 1.2 assert cfg.pitch == 3 assert cfg.volume == 0.7 # ── is_dataclass 验证 ─────────────────────────────────────────────────────── class TestTtsConfigStructure: """TtsConfig 结构验证.""" def test_is_dataclass(self): """是dataclass.""" from dataclasses import is_dataclass assert is_dataclass(TtsConfig) def test_equality(self): """相同配置相等.""" c1 = TtsConfig(enabled=True, voice_id="v1") c2 = TtsConfig(enabled=True, voice_id="v1") assert c1 == c2 def test_inequality(self): """不同配置不等.""" c1 = TtsConfig(enabled=True, voice_id="v1") c2 = TtsConfig(enabled=True, voice_id="v2") assert c1 != c2