test(wave150): tts_config 单测补全 +57 #1073
+353
-119
@@ -1,153 +1,387 @@
|
||||
"""TtsConfig 配音配置模型单测."""
|
||||
"""TTS 配音配置模型单元测试."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.tts_config import TtsConfig
|
||||
|
||||
# ── 默认值测试 ────────────────────────────────────────────────────────────────
|
||||
|
||||
|
||||
class TestTtsConfigDefaults:
|
||||
def test_default_values(self):
|
||||
config = TtsConfig()
|
||||
assert config.enabled is False
|
||||
assert config.voice_id == ""
|
||||
assert config.speed == 1.0
|
||||
assert config.pitch == 0.0
|
||||
assert config.volume == 0.8
|
||||
assert config.text == ""
|
||||
assert config.align_mode == "full"
|
||||
assert config.overlap_mode == "replace"
|
||||
"""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"
|
||||
|
||||
|
||||
class TestTtsConfigParse:
|
||||
def test_parse_none(self):
|
||||
config = TtsConfig.parse(None)
|
||||
assert config.enabled is False
|
||||
assert isinstance(config, TtsConfig)
|
||||
# ── parse - 基础场景测试 ─────────────────────────────────────────────────────
|
||||
|
||||
def test_parse_empty_dict(self):
|
||||
config = TtsConfig.parse({})
|
||||
assert config.enabled is False
|
||||
|
||||
def test_parse_not_dict(self):
|
||||
config = TtsConfig.parse("not a dict")
|
||||
assert config.enabled is False
|
||||
class TestTtsConfigParseBasic:
|
||||
"""TtsConfig.parse 基础场景测试."""
|
||||
|
||||
def test_parse_enabled_false_returns_disabled(self):
|
||||
# 即使传了其他参数,enabled=False 就直接返回禁用
|
||||
config = TtsConfig.parse({"enabled": False, "voice_id": "v1", "speed": 1.5})
|
||||
assert config.enabled is False
|
||||
assert config.voice_id == ""
|
||||
assert config.speed == 1.0
|
||||
def test_none_data(self):
|
||||
"""None输入返回默认配置(禁用)."""
|
||||
cfg = TtsConfig.parse(None)
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_parse_enabled_true_with_all_fields(self):
|
||||
config = TtsConfig.parse(
|
||||
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": "female_warm",
|
||||
"speed": 1.5,
|
||||
"pitch": 2.0,
|
||||
"voice_id": "v_test",
|
||||
"speed": 1.2,
|
||||
"pitch": 2.5,
|
||||
"volume": 0.9,
|
||||
"text": "你好世界",
|
||||
"text": "大家好",
|
||||
"align_mode": "subtitle",
|
||||
"overlap_mode": "mix",
|
||||
}
|
||||
)
|
||||
assert config.enabled is True
|
||||
assert config.voice_id == "female_warm"
|
||||
assert config.speed == 1.5
|
||||
assert config.pitch == 2.0
|
||||
assert config.volume == 0.9
|
||||
assert config.text == "你好世界"
|
||||
assert config.align_mode == "subtitle"
|
||||
assert config.overlap_mode == "mix"
|
||||
|
||||
def test_parse_enabled_not_bool(self):
|
||||
config = TtsConfig.parse({"enabled": "true", "voice_id": "v1"})
|
||||
assert config.enabled is False # 非 bool 值视为 False
|
||||
|
||||
def test_parse_voice_id_not_string(self):
|
||||
config = TtsConfig.parse({"enabled": True, "voice_id": 123})
|
||||
assert config.voice_id == ""
|
||||
|
||||
def test_parse_speed_not_number(self):
|
||||
config = TtsConfig.parse({"enabled": True, "speed": "fast"})
|
||||
assert config.speed == 1.0
|
||||
|
||||
def test_parse_pitch_not_number(self):
|
||||
config = TtsConfig.parse({"enabled": True, "pitch": "high"})
|
||||
assert config.pitch == 0.0
|
||||
|
||||
def test_parse_volume_not_number(self):
|
||||
config = TtsConfig.parse({"enabled": True, "volume": "loud"})
|
||||
assert config.volume == 0.8
|
||||
|
||||
def test_parse_text_not_string(self):
|
||||
config = TtsConfig.parse({"enabled": True, "text": 12345})
|
||||
assert config.text == ""
|
||||
|
||||
def test_parse_invalid_align_mode(self):
|
||||
config = TtsConfig.parse({"enabled": True, "align_mode": "invalid"})
|
||||
assert config.align_mode == "full"
|
||||
|
||||
def test_parse_invalid_overlap_mode(self):
|
||||
config = TtsConfig.parse({"enabled": True, "overlap_mode": "invalid"})
|
||||
assert config.overlap_mode == "replace"
|
||||
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"
|
||||
|
||||
|
||||
class TestTtsConfigClamp:
|
||||
def test_speed_below_min(self):
|
||||
config = TtsConfig.parse({"enabled": True, "speed": 0.1})
|
||||
assert config.speed == 0.5
|
||||
# ── parse - 类型校验测试 ─────────────────────────────────────────────────────
|
||||
|
||||
def test_speed_above_max(self):
|
||||
config = TtsConfig.parse({"enabled": True, "speed": 3.0})
|
||||
assert config.speed == 2.0
|
||||
|
||||
def test_speed_within_range(self):
|
||||
config = TtsConfig.parse({"enabled": True, "speed": 1.2})
|
||||
assert config.speed == 1.2
|
||||
class TestTtsConfigParseTypeChecks:
|
||||
"""TtsConfig.parse 类型校验测试."""
|
||||
|
||||
def test_speed_boundary_values(self):
|
||||
config_low = TtsConfig.parse({"enabled": True, "speed": 0.5})
|
||||
assert config_low.speed == 0.5
|
||||
config_high = TtsConfig.parse({"enabled": True, "speed": 2.0})
|
||||
assert config_high.speed == 2.0
|
||||
def test_enabled_not_bool(self):
|
||||
"""enabled不是bool时视为False."""
|
||||
cfg = TtsConfig.parse({"enabled": "true", "voice_id": "v1"})
|
||||
assert cfg.enabled is False
|
||||
|
||||
def test_pitch_below_min(self):
|
||||
config = TtsConfig.parse({"enabled": True, "pitch": -20})
|
||||
assert config.pitch == -12
|
||||
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_pitch_above_max(self):
|
||||
config = TtsConfig.parse({"enabled": True, "pitch": 20})
|
||||
assert config.pitch == 12
|
||||
def test_voice_id_not_string(self):
|
||||
"""voice_id不是字符串时回退到空."""
|
||||
cfg = TtsConfig.parse({"enabled": True, "voice_id": 123})
|
||||
assert cfg.voice_id == ""
|
||||
|
||||
def test_pitch_within_range(self):
|
||||
config = TtsConfig.parse({"enabled": True, "pitch": -3.5})
|
||||
assert config.pitch == -3.5
|
||||
def test_speed_not_number(self):
|
||||
"""speed不是数字时回退到1.0."""
|
||||
cfg = TtsConfig.parse({"enabled": True, "speed": "fast"})
|
||||
assert cfg.speed == 1.0
|
||||
|
||||
def test_volume_below_min(self):
|
||||
config = TtsConfig.parse({"enabled": True, "volume": -0.5})
|
||||
assert config.volume == 0.0
|
||||
def test_speed_int_accepted(self):
|
||||
"""整数speed也接受."""
|
||||
cfg = TtsConfig.parse({"enabled": True, "speed": 2})
|
||||
assert cfg.speed == 2.0
|
||||
|
||||
def test_volume_above_max(self):
|
||||
config = TtsConfig.parse({"enabled": True, "volume": 2.0})
|
||||
assert config.volume == 1.0
|
||||
def test_pitch_not_number(self):
|
||||
"""pitch不是数字时回退到0."""
|
||||
cfg = TtsConfig.parse({"enabled": True, "pitch": "high"})
|
||||
assert cfg.pitch == 0.0
|
||||
|
||||
def test_volume_within_range(self):
|
||||
config = TtsConfig.parse({"enabled": True, "volume": 0.5})
|
||||
assert config.volume == 0.5
|
||||
def test_pitch_int_accepted(self):
|
||||
"""整数pitch也接受."""
|
||||
cfg = TtsConfig.parse({"enabled": True, "pitch": 5})
|
||||
assert cfg.pitch == 5.0
|
||||
|
||||
def test_int_speed_converted_to_float(self):
|
||||
config = TtsConfig.parse({"enabled": True, "speed": 1})
|
||||
assert isinstance(config.speed, float)
|
||||
assert config.speed == 1.0
|
||||
def test_volume_not_number(self):
|
||||
"""volume不是数字时回退到0.8."""
|
||||
cfg = TtsConfig.parse({"enabled": True, "volume": "loud"})
|
||||
assert cfg.volume == 0.8
|
||||
|
||||
def test_int_pitch_converted_to_float(self):
|
||||
config = TtsConfig.parse({"enabled": True, "pitch": 2})
|
||||
assert isinstance(config.pitch, float)
|
||||
assert config.pitch == 2.0
|
||||
def test_volume_int_accepted(self):
|
||||
"""整数volume也接受."""
|
||||
cfg = TtsConfig.parse({"enabled": True, "volume": 1})
|
||||
assert cfg.volume == 1.0
|
||||
|
||||
def test_int_volume_converted_to_float(self):
|
||||
config = TtsConfig.parse({"enabled": True, "volume": 1})
|
||||
assert isinstance(config.volume, float)
|
||||
assert config.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
|
||||
|
||||
Reference in New Issue
Block a user