Files
xiaoxia-saas/tests/unit/test_tts_config.py
xiaoxia c73c4be367
CI/CD Pipeline / CI Gate (push) Has been skipped
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Failing after 5m18s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m32s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 1m34s
CI/CD Pipeline / Unit Tests (push) Failing after 5m48s
CI/CD Pipeline / Integration Tests (push) Successful in 3m23s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m15s
CI/CD Pipeline / Frontend Unit Tests (push) Failing after 2m45s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 19m3s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m59s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 6m36s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m59s
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (push) Has been cancelled
CI/CD Pipeline / Canary Release to Production (push) Has been cancelled
test(wave150): tts_config 单测补全 +57 (#1073)
2026-07-28 12:25:56 +08:00

388 lines
13 KiB
Python
Executable File
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""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为整数时视为非boolPython里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