ef8b747c7b
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 35s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m11s
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 3m26s
CI/CD Pipeline / Unit Tests (push) Successful in 4m25s
CI/CD Pipeline / Integration Tests (push) Successful in 1m32s
CI Build & Deploy Pipeline / Build Staging API Image (push) Waiting to run
CI Build & Deploy Pipeline / Build Staging Web Image (push) Waiting to run
CI Build & Deploy Pipeline / Build Staging Worker Image (push) Waiting to run
CI Build & Deploy Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Blocked by required conditions
CI Build & Deploy Pipeline / Staging E2E Tests (push) Blocked by required conditions
CI Build & Deploy Pipeline / Staging API Integration Tests (push) Blocked by required conditions
CI Build & Deploy Pipeline / Build Production API Image (push) Waiting to run
CI Build & Deploy Pipeline / Build Production Web Image (push) Waiting to run
CI Build & Deploy Pipeline / Build Production Worker Image (push) Waiting to run
CI Build & Deploy Pipeline / Deploy Production (push) Blocked by required conditions
CI Build & Deploy Pipeline / Production Browser E2E (push) Blocked by required conditions
Co-authored-by: xiaoxia <dev@xiaoxiajianji.com> Co-committed-by: xiaoxia <dev@xiaoxiajianji.com>
175 lines
6.1 KiB
Python
Executable File
175 lines
6.1 KiB
Python
Executable File
"""CosyVoice TTS 适配器单元测试."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from unittest.mock import MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from packages.domain.tts_config import TtsConfig
|
|
|
|
|
|
class TestTtsConfig:
|
|
"""TTS 配置解析测试."""
|
|
|
|
def test_parse_full_config(self):
|
|
"""完整配置解析."""
|
|
cfg = TtsConfig.parse(
|
|
{
|
|
"enabled": True,
|
|
"voice_id": "longxiaochun_v3",
|
|
"text": "你好世界",
|
|
"speed": 1.2,
|
|
"pitch": 2.0,
|
|
"volume": 0.7,
|
|
"align_mode": "full",
|
|
"overlap_mode": "mix",
|
|
}
|
|
)
|
|
assert cfg.enabled is True
|
|
assert cfg.voice_id == "longxiaochun_v3"
|
|
assert cfg.text == "你好世界"
|
|
assert cfg.speed == 1.2
|
|
assert cfg.pitch == 2.0
|
|
assert cfg.volume == 0.7
|
|
assert cfg.align_mode == "full"
|
|
assert cfg.overlap_mode == "mix"
|
|
|
|
def test_parse_disabled(self):
|
|
"""禁用状态."""
|
|
cfg = TtsConfig.parse({"enabled": False})
|
|
assert cfg.enabled is False
|
|
|
|
def test_parse_none(self):
|
|
"""空配置."""
|
|
cfg = TtsConfig.parse(None)
|
|
assert cfg.enabled is False
|
|
|
|
def test_parse_empty_text_still_enabled(self):
|
|
"""有 enabled 但无 text,配置仍然有效(调用方判断是否有文本)."""
|
|
cfg = TtsConfig.parse({"enabled": True, "voice_id": "test"})
|
|
assert cfg.enabled is True
|
|
assert cfg.text == ""
|
|
|
|
def test_speed_clamp(self):
|
|
"""语速边界钳制."""
|
|
cfg = TtsConfig.parse({"enabled": True, "speed": 3.0})
|
|
assert cfg.speed == 2.0
|
|
|
|
cfg2 = TtsConfig.parse({"enabled": True, "speed": 0.1})
|
|
assert cfg2.speed == 0.5
|
|
|
|
def test_volume_clamp(self):
|
|
"""音量边界钳制."""
|
|
cfg = TtsConfig.parse({"enabled": True, "volume": 2.0})
|
|
assert cfg.volume == 1.0
|
|
|
|
def test_invalid_align_mode(self):
|
|
"""无效对齐模式回退到默认."""
|
|
cfg = TtsConfig.parse({"enabled": True, "align_mode": "invalid"})
|
|
assert cfg.align_mode == "full"
|
|
|
|
|
|
class TestCosyVoiceTtsAdapter:
|
|
"""CosyVoice TTS 适配器测试."""
|
|
|
|
def test_import_ok(self):
|
|
"""适配器能正常导入."""
|
|
from packages.adapters.tts.cosyvoice_tts_service import CosyVoiceTtsService
|
|
|
|
assert CosyVoiceTtsService is not None
|
|
|
|
def test_provider_name(self):
|
|
"""provider_name 属性."""
|
|
from packages.adapters.tts.cosyvoice_tts_service import CosyVoiceTtsService
|
|
|
|
# 用 mock 替换底层 CosyVoiceService
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = CosyVoiceTtsService()
|
|
assert svc.provider_name == "cosyvoice"
|
|
|
|
def test_available_voices(self):
|
|
"""可用音色列表来自预设音色."""
|
|
from packages.adapters.tts.cosyvoice_tts_service import CosyVoiceTtsService
|
|
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = CosyVoiceTtsService()
|
|
voices = svc.available_voices()
|
|
assert len(voices) > 0
|
|
assert "longxiaochun_v3" in voices
|
|
assert "longxiaoxia_v3" in voices
|
|
|
|
def test_estimate_duration(self):
|
|
"""时长估算."""
|
|
from packages.adapters.tts.cosyvoice_tts_service import CosyVoiceTtsService
|
|
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = CosyVoiceTtsService()
|
|
d = svc.estimate_duration("你好世界", speed=1.0)
|
|
assert d > 0
|
|
# 4 个字,4 字/秒 = 1 秒
|
|
assert abs(d - 1.0) < 0.1
|
|
|
|
def test_estimate_duration_speed(self):
|
|
"""语速影响时长估算."""
|
|
from packages.adapters.tts.cosyvoice_tts_service import CosyVoiceTtsService
|
|
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = CosyVoiceTtsService()
|
|
d_normal = svc.estimate_duration("你好世界", speed=1.0)
|
|
d_fast = svc.estimate_duration("你好世界", speed=2.0)
|
|
assert d_fast < d_normal
|
|
assert abs(d_fast - d_normal / 2.0) < 0.01
|
|
|
|
def test_synthesize_empty_text_raises(self):
|
|
"""空文本抛出异常."""
|
|
from packages.adapters.tts.cosyvoice_tts_service import CosyVoiceTtsService
|
|
from packages.ports.tts_service import TtsError
|
|
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = CosyVoiceTtsService()
|
|
with pytest.raises(TtsError, match="文本不能为空"):
|
|
svc.synthesize(" ")
|
|
|
|
|
|
class TestTtsServiceFactory:
|
|
"""TTS 服务工厂测试."""
|
|
|
|
def test_mock_provider(self):
|
|
"""mock provider 正常."""
|
|
from apps.worker.services.tts_service_factory import get_tts_service
|
|
|
|
svc = get_tts_service("mock")
|
|
assert svc.provider_name == "mock"
|
|
|
|
def test_cosyvoice_provider(self):
|
|
"""cosyvoice provider 注册正常."""
|
|
from apps.worker.services.tts_service_factory import get_tts_service
|
|
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = get_tts_service("cosyvoice")
|
|
assert svc.provider_name == "cosyvoice"
|
|
|
|
def test_aliyun_alias(self):
|
|
"""aliyun 别名映射到 cosyvoice."""
|
|
from apps.worker.services.tts_service_factory import get_tts_service
|
|
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = get_tts_service("aliyun")
|
|
assert svc.provider_name == "cosyvoice"
|
|
|
|
def test_dashscope_alias(self):
|
|
"""dashscope 别名映射到 cosyvoice."""
|
|
from apps.worker.services.tts_service_factory import get_tts_service
|
|
|
|
with patch("packages.application.cosyvoice_service.CosyVoiceService"):
|
|
svc = get_tts_service("dashscope")
|
|
assert svc.provider_name == "cosyvoice"
|
|
|
|
def test_unknown_fallback_to_mock(self):
|
|
"""未知 provider 回退到 mock."""
|
|
from apps.worker.services.tts_service_factory import get_tts_service
|
|
|
|
svc = get_tts_service("unknown_provider")
|
|
assert svc.provider_name == "mock"
|