Compare commits

...

2 Commits

Author SHA1 Message Date
CI Bot bb8ed8df0c style: auto-format with black + isort + prettier [skip ci-format-check]
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 7s
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m15s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m10s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 51s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m26s
PR Automation / Auto Approve on CI Green (pull_request) Successful in 3m15s
AI Code Review / AI Code Review (pull_request) Successful in 3m23s
CI/CD Pipeline / Frontend Lint (pull_request) Has been skipped
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been skipped
CI/CD Pipeline / PR Build Web Image (pull_request) Has been skipped
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Validate - Code Quality (pull_request) Successful in 6m22s
CI/CD Pipeline / PR Build API Image (pull_request) Successful in 3m6s
CI/CD Pipeline / PR Build Worker Image (pull_request) Successful in 3m7s
CI/CD Pipeline / Unit Tests (pull_request) Successful in 3m47s
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been skipped
CI/CD Pipeline / Integration Tests (pull_request) Successful in 3m19s
CI/CD Pipeline / Build Production API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Successful in 32s
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
ACR Cleanup / ACR Image Cleanup (pull_request_target) Has been cancelled
Preview Cleanup / Cleanup Preview Environment (pull_request) Successful in 58s
2026-07-29 10:38:08 +00:00
xiaoxia fa991831b1 test(wave183): voice_presets 配音音色预设 +59测
CI/CD Pipeline / Build Staging API Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Web Image (pull_request) Has been skipped
CI/CD Pipeline / Build Staging Worker Image (pull_request) Has been skipped
CI/CD Pipeline / Check if frontend-only change (pull_request) Successful in 52s
CI/CD Pipeline / Validate - Migration (alembic) (pull_request) Successful in 1m42s
Preview Deploy / Deploy Preview Environment (pull_request) Successful in 53s
CI/CD Pipeline / Validate - Type Check (mypy) (pull_request) Successful in 1m48s
AI Code Review / AI Code Review (pull_request) Successful in 1m45s
PR Automation / Auto Merge on CI Green + Approved (pull_request) Successful in 1m49s
CI/CD Pipeline / Validate - Code Quality (pull_request) Has been cancelled
CI/CD Pipeline / Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Lint (pull_request) Has been cancelled
CI/CD Pipeline / Frontend Unit Tests (pull_request) Has been cancelled
CI/CD Pipeline / PR Build API Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Web Image (pull_request) Has been cancelled
CI/CD Pipeline / PR Build Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (pull_request) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been cancelled
CI/CD Pipeline / Build Production API Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Web Image (pull_request) Has been cancelled
CI/CD Pipeline / Build Production Worker Image (pull_request) Has been cancelled
CI/CD Pipeline / Deploy Production (pull_request) Has been cancelled
CI/CD Pipeline / Production Browser E2E (pull_request) Has been cancelled
CI/CD Pipeline / ACR Image Cleanup (pull_request) Has been cancelled
CI/CD Pipeline / Canary Release to Production (pull_request) Has been cancelled
CI/CD Pipeline / CI Gate (pull_request) Has been cancelled
PR Automation / Auto Approve on CI Green (pull_request) Has been cancelled
2026-07-29 18:32:07 +08:00
+368
View File
@@ -0,0 +1,368 @@
"""voice_presets 配音音色预设模块单测."""
import pytest
from domain.voice_presets import (
MOCK_VOICES,
VoiceGender,
VoicePreset,
VoiceStyle,
get_default_voice,
get_voice,
list_voices,
)
# ── 枚举测试 ──────────────────────────────────────────────────────────────────
class TestVoiceGender:
"""VoiceGender 音色性别枚举"""
def test_enum_values(self):
assert VoiceGender.MALE.value == "male"
assert VoiceGender.FEMALE.value == "female"
assert VoiceGender.CHILD.value == "child"
def test_is_str(self):
assert isinstance(VoiceGender.MALE, str)
assert VoiceGender.FEMALE == "female"
def test_from_string(self):
assert VoiceGender("male") == VoiceGender.MALE
assert VoiceGender("child") == VoiceGender.CHILD
def test_invalid_raises(self):
with pytest.raises(ValueError):
VoiceGender("unknown")
class TestVoiceStyle:
"""VoiceStyle 音色风格枚举"""
def test_enum_values(self):
assert VoiceStyle.STABLE.value == "stable"
assert VoiceStyle.LIVELY.value == "lively"
assert VoiceStyle.CUSTOMER_SERVICE.value == "customer_service"
assert VoiceStyle.NARRATION.value == "narration"
assert VoiceStyle.NEWS.value == "news"
assert VoiceStyle.STORY.value == "story"
def test_is_str(self):
assert isinstance(VoiceStyle.NARRATION, str)
assert VoiceStyle.STORY == "story"
def test_from_string(self):
assert VoiceStyle("news") == VoiceStyle.NEWS
def test_invalid_raises(self):
with pytest.raises(ValueError):
VoiceStyle("rock")
# ── VoicePreset dataclass ─────────────────────────────────────────────────────
class TestVoicePreset:
"""VoicePreset 音色预设 dataclass"""
def test_minimal_creation(self):
v = VoicePreset(voice_id="test_voice", name="测试音色")
assert v.voice_id == "test_voice"
assert v.name == "测试音色"
# 默认值
assert v.gender == VoiceGender.FEMALE
assert v.style == VoiceStyle.NARRATION
assert v.description == ""
assert v.provider == "mock"
assert v.provider_voice_id == ""
assert v.default_speed == 1.0
assert v.default_pitch == 0.0
assert v.sample_rate == 22050
assert v.language == "zh-CN"
def test_full_creation(self):
v = VoicePreset(
voice_id="male_deep",
name="深沉男声",
gender=VoiceGender.MALE,
style=VoiceStyle.STABLE,
description="非常深沉的男声",
provider="aliyun",
provider_voice_id="zhiyuan",
default_speed=0.8,
default_pitch=-1.0,
sample_rate=16000,
language="zh-CN",
)
assert v.voice_id == "male_deep"
assert v.gender == VoiceGender.MALE
assert v.style == VoiceStyle.STABLE
assert v.provider == "aliyun"
assert v.default_speed == 0.8
assert v.sample_rate == 16000
def test_str_gender_creation(self):
# 用字符串值创建也可以(因为是 StrEnum)
v = VoicePreset(voice_id="v1", name="V1", gender="male")
assert v.gender == VoiceGender.MALE
def test_str_style_creation(self):
v = VoicePreset(voice_id="v1", name="V1", style="news")
assert v.style == VoiceStyle.NEWS
def test_equality(self):
v1 = VoicePreset(voice_id="same", name="同名")
v2 = VoicePreset(voice_id="same", name="同名")
assert v1 == v2
def test_inequality(self):
v1 = VoicePreset(voice_id="a", name="A")
v2 = VoicePreset(voice_id="b", name="B")
assert v1 != v2
def test_slots_no_extra_attrs(self):
v = VoicePreset(voice_id="test", name="Test")
with pytest.raises(AttributeError):
v.nonexistent_field = "value"
# ── MOCK_VOICES 列表 ─────────────────────────────────────────────────────────
class TestMockVoices:
"""Mock 音色预设列表"""
def test_not_empty(self):
assert len(MOCK_VOICES) > 0
def test_count(self):
assert len(MOCK_VOICES) == 8
def test_all_are_voice_preset(self):
for v in MOCK_VOICES:
assert isinstance(v, VoicePreset)
def test_unique_voice_ids(self):
ids = [v.voice_id for v in MOCK_VOICES]
assert len(ids) == len(set(ids))
def test_female_warm_preset(self):
v = next(v for v in MOCK_VOICES if v.voice_id == "female_warm")
assert v.name == "温暖女声"
assert v.gender == VoiceGender.FEMALE
assert v.style == VoiceStyle.NARRATION
assert v.default_speed == 1.0
assert "温柔" in v.description
def test_male_stable_preset(self):
v = next(v for v in MOCK_VOICES if v.voice_id == "male_stable")
assert v.name == "沉稳男声"
assert v.gender == VoiceGender.MALE
assert v.style == VoiceStyle.STABLE
assert v.default_speed == 0.9
def test_female_lively_preset(self):
v = next(v for v in MOCK_VOICES if v.voice_id == "female_lively")
assert v.gender == VoiceGender.FEMALE
assert v.style == VoiceStyle.LIVELY
assert v.default_speed == 1.2
assert v.default_pitch == 2.0
def test_child_cute_preset(self):
v = next(v for v in MOCK_VOICES if v.voice_id == "child_cute")
assert v.gender == VoiceGender.CHILD
assert v.style == VoiceStyle.STORY
assert v.default_pitch == 4.0
def test_all_mock_provider(self):
for v in MOCK_VOICES:
assert v.provider == "mock"
def test_all_have_provider_voice_id(self):
for v in MOCK_VOICES:
assert v.provider_voice_id != ""
def test_all_chinese(self):
for v in MOCK_VOICES:
assert v.language == "zh-CN"
# ── get_voice ─────────────────────────────────────────────────────────────────
class TestGetVoice:
"""get_voice 函数"""
def test_get_existing_voice(self):
v = get_voice("female_warm")
assert v is not None
assert v.voice_id == "female_warm"
assert v.name == "温暖女声"
def test_get_male_stable(self):
v = get_voice("male_stable")
assert v is not None
assert v.gender == VoiceGender.MALE
def test_get_child_cute(self):
v = get_voice("child_cute")
assert v is not None
assert v.gender == VoiceGender.CHILD
def test_get_nonexistent_returns_none(self):
v = get_voice("nonexistent_voice")
assert v is None
def test_get_empty_string_returns_none(self):
v = get_voice("")
assert v is None
def test_non_mock_provider_returns_none(self):
v = get_voice("female_warm", provider="aliyun")
assert v is None
def test_non_mock_provider_nonexistent(self):
v = get_voice("whatever", provider="xunfei")
assert v is None
def test_mock_provider_explicit(self):
v = get_voice("female_warm", provider="mock")
assert v is not None
assert v.voice_id == "female_warm"
def test_returns_same_instance(self):
# 应该返回同一个对象(缓存的)
v1 = get_voice("female_warm")
v2 = get_voice("female_warm")
assert v1 is v2
# ── list_voices ───────────────────────────────────────────────────────────────
class TestListVoices:
"""list_voices 函数"""
def test_no_filters_returns_all(self):
result = list_voices()
assert len(result) == len(MOCK_VOICES)
assert len(result) == 8
def test_filter_by_gender_male(self):
result = list_voices(gender="male")
assert len(result) > 0
for v in result:
assert v.gender == VoiceGender.MALE
def test_filter_by_gender_female(self):
result = list_voices(gender="female")
assert len(result) > 0
for v in result:
assert v.gender == VoiceGender.FEMALE
def test_filter_by_gender_child(self):
result = list_voices(gender="child")
assert len(result) == 1
assert result[0].voice_id == "child_cute"
def test_filter_by_gender_invalid_returns_empty(self):
result = list_voices(gender="alien")
assert len(result) == 0
def test_filter_by_style_stable(self):
result = list_voices(style="stable")
assert len(result) > 0
for v in result:
assert v.style == VoiceStyle.STABLE
def test_filter_by_style_lively(self):
result = list_voices(style="lively")
assert len(result) == 1
assert result[0].voice_id == "female_lively"
def test_filter_by_style_story(self):
result = list_voices(style="story")
assert len(result) >= 2
for v in result:
assert v.style == VoiceStyle.STORY
def test_filter_by_style_invalid_returns_empty(self):
result = list_voices(style="punk")
assert len(result) == 0
def test_filter_by_provider_mock(self):
result = list_voices(provider="mock")
assert len(result) == len(MOCK_VOICES)
def test_filter_by_provider_other_returns_empty(self):
result = list_voices(provider="aliyun")
assert len(result) == 0
def test_filter_by_keyword_name(self):
result = list_voices(keyword="女声")
assert len(result) > 0
for v in result:
assert "女声" in v.name or "女声" in v.description or "女声" in v.voice_id
def test_filter_by_keyword_description(self):
result = list_voices(keyword="商务")
assert len(result) > 0
# 沉稳男声描述里有"商务"
def test_filter_by_keyword_voice_id(self):
result = list_voices(keyword="male_stable")
assert len(result) == 1
assert result[0].voice_id == "male_stable"
def test_filter_by_keyword_case_insensitive(self):
result1 = list_voices(keyword="Female")
result2 = list_voices(keyword="female")
assert len(result1) == len(result2)
def test_filter_by_keyword_nonexistent(self):
result = list_voices(keyword="不存在的关键词999")
assert len(result) == 0
def test_combined_gender_and_style(self):
result = list_voices(gender="female", style="lively")
assert len(result) == 1
assert result[0].voice_id == "female_lively"
def test_combined_gender_style_keyword(self):
result = list_voices(gender="male", style="story", keyword="磁性")
assert len(result) == 1
assert result[0].voice_id == "male_magnetic"
def test_combined_no_match(self):
result = list_voices(gender="child", style="news")
assert len(result) == 0
def test_returns_new_list(self):
# 修改返回值不应影响原始列表
result = list_voices()
result.clear()
assert len(MOCK_VOICES) == 8
# ── get_default_voice ─────────────────────────────────────────────────────────
class TestGetDefaultVoice:
"""get_default_voice 函数"""
def test_returns_voice_preset(self):
v = get_default_voice()
assert isinstance(v, VoicePreset)
def test_returns_first_mock_voice(self):
v = get_default_voice()
assert v == MOCK_VOICES[0]
def test_default_is_female_warm(self):
v = get_default_voice()
assert v.voice_id == "female_warm"
assert v.gender == VoiceGender.FEMALE
def test_multiple_calls_same(self):
v1 = get_default_voice()
v2 = get_default_voice()
assert v1 is v2