feat: 配音列表 API 增强 — 预置音色 + 克隆音色统一接口 #164
@@ -1,11 +1,20 @@
|
||||
"""Voice library CRUD routes."""
|
||||
"""Voice library CRUD routes — Phase 3 增强版.
|
||||
|
||||
支持预置音色和克隆音色的统一列表。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Optional
|
||||
from typing import Literal, Optional
|
||||
|
||||
from app.auth import AuthenticatedUser, get_current_user
|
||||
from app.dependencies import get_db_session, get_user_repository
|
||||
from app.schemas.voice import (
|
||||
PresetVoiceItemResponse,
|
||||
PresetVoiceListResponse,
|
||||
UnifiedVoiceItemResponse,
|
||||
UnifiedVoiceListResponse,
|
||||
)
|
||||
from app.schemas.voice_library import (
|
||||
CreateVoiceLibraryRequest,
|
||||
ListVoiceLibraryResponse,
|
||||
@@ -26,6 +35,7 @@ from packages.application.voice_library.use_cases import (
|
||||
QuotaExceededError,
|
||||
UpdateVoiceLibraryUseCase,
|
||||
)
|
||||
from packages.domain.preset_voices import PRESET_VOICES
|
||||
from packages.ports.user_repository import UserRepository
|
||||
|
||||
router = APIRouter()
|
||||
@@ -55,6 +65,45 @@ def _to_response(item) -> VoiceLibraryItemResponse:
|
||||
)
|
||||
|
||||
|
||||
def _to_unified_response(item) -> UnifiedVoiceItemResponse:
|
||||
"""将数据库音色转换为统一响应格式。"""
|
||||
return UnifiedVoiceItemResponse(
|
||||
id=item.id,
|
||||
type="clone",
|
||||
name=item.name,
|
||||
description=item.text,
|
||||
gender="unknown",
|
||||
language="zh-CN",
|
||||
voice_id=item.voice_id,
|
||||
voice_provider=item.voice_provider or "cosyvoice",
|
||||
audio_url=item.audio_url,
|
||||
duration=item.duration,
|
||||
file_size=item.file_size,
|
||||
status=item.status,
|
||||
tags=item.tags,
|
||||
user_id=item.user_id,
|
||||
project_id=item.project_id,
|
||||
created_at=item.created_at,
|
||||
updated_at=item.updated_at,
|
||||
)
|
||||
|
||||
|
||||
def _preset_to_unified_response(preset) -> UnifiedVoiceItemResponse:
|
||||
"""将预置音色转换为统一响应格式。"""
|
||||
return UnifiedVoiceItemResponse(
|
||||
id=preset.voice_id,
|
||||
type="preset",
|
||||
name=preset.name,
|
||||
description=preset.description,
|
||||
gender=preset.gender,
|
||||
language=preset.language,
|
||||
voice_id=preset.voice_id,
|
||||
voice_provider="cosyvoice",
|
||||
preview_url=preset.preview_url,
|
||||
tags=preset.tags or [],
|
||||
)
|
||||
|
||||
|
||||
def _get_user_plan(user_id: str, user_repository: UserRepository) -> str:
|
||||
user = user_repository.find_by_id(user_id)
|
||||
if user is None:
|
||||
@@ -62,14 +111,107 @@ def _get_user_plan(user_id: str, user_repository: UserRepository) -> str:
|
||||
return getattr(user, "subscription_plan", "free") or "free"
|
||||
|
||||
|
||||
@router.get("", response_model=ListVoiceLibraryResponse)
|
||||
def list_voices(
|
||||
# ==================== 统一配音列表(预置 + 克隆)====================
|
||||
|
||||
|
||||
@router.get("", response_model=UnifiedVoiceListResponse)
|
||||
def list_voices_unified(
|
||||
type: Optional[Literal["preset", "clone"]] = Query(
|
||||
None,
|
||||
description="音色类型过滤:preset=仅预置,clone=仅克隆,不传=全部",
|
||||
),
|
||||
status_filter: Optional[str] = Query(None, alias="status"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
voice_repository: SQLAlchemyVoiceLibraryRepository = Depends(_get_voice_repository),
|
||||
) -> UnifiedVoiceListResponse:
|
||||
"""获取配音列表(预置音色 + 用户克隆音色)。
|
||||
|
||||
- 不传 type:返回预置音色 + 用户克隆音色,预置音色在前
|
||||
- type=preset:仅返回预置音色
|
||||
- type=clone:仅返回用户克隆音色
|
||||
"""
|
||||
user_id = authenticated_user.user.id
|
||||
items: list[UnifiedVoiceItemResponse] = []
|
||||
preset_count = 0
|
||||
clone_count = 0
|
||||
|
||||
has_preset = type is None or type == "preset"
|
||||
has_clone = type is None or type == "clone"
|
||||
|
||||
# 获取预置音色
|
||||
if has_preset:
|
||||
preset_items = [_preset_to_unified_response(p) for p in PRESET_VOICES]
|
||||
preset_count = len(preset_items)
|
||||
|
||||
# 获取克隆音色
|
||||
if has_clone:
|
||||
use_case = ListVoiceLibraryUseCase(voice_repository)
|
||||
clone_items_raw = use_case.execute(user_id, status=status_filter, skip=skip, limit=limit)
|
||||
clone_items = [_to_unified_response(i) for i in clone_items_raw]
|
||||
clone_count = voice_repository.count_by_user(user_id, status=status_filter) if status_filter else voice_repository.count_by_user(user_id)
|
||||
|
||||
# 组装结果
|
||||
if type == "preset":
|
||||
items = preset_items[skip : skip + limit]
|
||||
total = preset_count
|
||||
elif type == "clone":
|
||||
items = clone_items
|
||||
total = clone_count
|
||||
else:
|
||||
# 全量模式:预置在前,克隆补位
|
||||
all_items = preset_items + clone_items
|
||||
total = preset_count + clone_count
|
||||
items = all_items[skip : skip + limit]
|
||||
|
||||
return UnifiedVoiceListResponse(
|
||||
items=items,
|
||||
total=total,
|
||||
preset_count=preset_count if has_preset else 0,
|
||||
clone_count=clone_count if has_clone else 0,
|
||||
)
|
||||
|
||||
|
||||
# ==================== 预置音色专用端点 ====================
|
||||
|
||||
|
||||
@router.get("/presets", response_model=PresetVoiceListResponse)
|
||||
def list_preset_voices() -> PresetVoiceListResponse:
|
||||
"""获取预置音色列表。
|
||||
|
||||
不需要认证,返回所有系统预置的 CosyVoice 音色。
|
||||
"""
|
||||
items = [
|
||||
PresetVoiceItemResponse(
|
||||
voice_id=p.voice_id,
|
||||
name=p.name,
|
||||
description=p.description,
|
||||
gender=p.gender,
|
||||
language=p.language,
|
||||
preview_url=p.preview_url,
|
||||
tags=p.tags or [],
|
||||
)
|
||||
for p in PRESET_VOICES
|
||||
]
|
||||
return PresetVoiceListResponse(items=items, total=len(items))
|
||||
|
||||
|
||||
# ==================== 原有 CRUD 端点(保持向后兼容)====================
|
||||
|
||||
|
||||
@router.get("/legacy", response_model=ListVoiceLibraryResponse)
|
||||
def list_voices_legacy(
|
||||
status_filter: Optional[str] = Query(None, alias="status"),
|
||||
skip: int = Query(0, ge=0),
|
||||
limit: int = Query(50, ge=1, le=200),
|
||||
authenticated_user: AuthenticatedUser = Depends(get_current_user),
|
||||
voice_repository: SQLAlchemyVoiceLibraryRepository = Depends(_get_voice_repository),
|
||||
) -> ListVoiceLibraryResponse:
|
||||
"""原有配音列表接口(仅返回用户克隆音色)。
|
||||
|
||||
保留用于向后兼容,新客户端请使用 GET /api/v1/voices。
|
||||
"""
|
||||
user_id = authenticated_user.user.id
|
||||
use_case = ListVoiceLibraryUseCase(voice_repository)
|
||||
items = use_case.execute(user_id, status=status_filter, skip=skip, limit=limit)
|
||||
|
||||
@@ -0,0 +1,127 @@
|
||||
"""统一配音响应 Schema — Phase 3 CosyVoice 集成.
|
||||
|
||||
支持预置音色和克隆音色的统一响应格式。
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class UnifiedVoiceItemResponse(BaseModel):
|
||||
"""统一配音项响应。
|
||||
|
||||
同时支持预置音色(type=preset)和克隆音色(type=clone)。
|
||||
"""
|
||||
|
||||
id: str
|
||||
"""音色 ID(预置音色为 voice_id,克隆音色为数据库 ID)"""
|
||||
|
||||
type: Literal["preset", "clone"]
|
||||
"""音色类型:preset=预置音色,clone=用户克隆音色"""
|
||||
|
||||
name: str
|
||||
"""音色展示名称"""
|
||||
|
||||
description: str = ""
|
||||
"""音色描述"""
|
||||
|
||||
gender: str = "unknown"
|
||||
"""性别:male/female/unknown"""
|
||||
|
||||
language: str = "zh-CN"
|
||||
"""语言代码"""
|
||||
|
||||
voice_id: str = ""
|
||||
"""CosyVoice 模型音色名"""
|
||||
|
||||
voice_provider: str = "cosyvoice"
|
||||
"""语音服务商"""
|
||||
|
||||
audio_url: str = ""
|
||||
"""音频 URL(克隆音色为上传的音频,预置音色为空)"""
|
||||
|
||||
preview_url: str = ""
|
||||
"""预览音频 URL(预置音色可能有)"""
|
||||
|
||||
duration: float = 0
|
||||
"""音频时长(秒)"""
|
||||
|
||||
file_size: int = 0
|
||||
"""文件大小(字节)"""
|
||||
|
||||
status: str = "completed"
|
||||
"""状态"""
|
||||
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
"""标签列表"""
|
||||
|
||||
# 克隆音色特有字段
|
||||
user_id: Optional[str] = None
|
||||
"""所属用户 ID(仅克隆音色)"""
|
||||
|
||||
project_id: Optional[str] = None
|
||||
"""所属项目 ID(仅克隆音色)"""
|
||||
|
||||
voice_clone_profile_id: Optional[str] = None
|
||||
"""关联的音色克隆档案 ID(仅克隆音色)"""
|
||||
|
||||
created_at: Optional[datetime] = None
|
||||
"""创建时间(仅克隆音色)"""
|
||||
|
||||
updated_at: Optional[datetime] = None
|
||||
"""更新时间(仅克隆音色)"""
|
||||
|
||||
|
||||
class UnifiedVoiceListResponse(BaseModel):
|
||||
"""统一配音列表响应。"""
|
||||
|
||||
items: list[UnifiedVoiceItemResponse]
|
||||
"""音色列表(预置音色在前)"""
|
||||
|
||||
total: int = 0
|
||||
"""总数"""
|
||||
|
||||
preset_count: int = 0
|
||||
"""预置音色数量"""
|
||||
|
||||
clone_count: int = 0
|
||||
"""克隆音色数量"""
|
||||
|
||||
|
||||
class PresetVoiceItemResponse(BaseModel):
|
||||
"""预置音色项响应。"""
|
||||
|
||||
voice_id: str
|
||||
"""CosyVoice 模型音色名"""
|
||||
|
||||
name: str
|
||||
"""中文展示名"""
|
||||
|
||||
description: str
|
||||
"""音色描述"""
|
||||
|
||||
gender: str
|
||||
"""性别"""
|
||||
|
||||
language: str = "zh-CN"
|
||||
"""语言代码"""
|
||||
|
||||
preview_url: str = ""
|
||||
"""预览音频 URL"""
|
||||
|
||||
tags: List[str] = Field(default_factory=list)
|
||||
"""标签列表"""
|
||||
|
||||
|
||||
class PresetVoiceListResponse(BaseModel):
|
||||
"""预置音色列表响应。"""
|
||||
|
||||
items: list[PresetVoiceItemResponse]
|
||||
"""预置音色列表"""
|
||||
|
||||
total: int = 0
|
||||
"""总数"""
|
||||
@@ -110,15 +110,17 @@ class SQLAlchemyVoiceLibraryRepository:
|
||||
self.session.commit()
|
||||
return True
|
||||
|
||||
def count_by_user(self, user_id: str) -> int:
|
||||
return (
|
||||
def count_by_user(self, user_id: str, *, status: Optional[str] = None) -> int:
|
||||
query = (
|
||||
self.session.query(VoiceLibraryModel)
|
||||
.filter(
|
||||
VoiceLibraryModel.user_id == user_id,
|
||||
VoiceLibraryModel.status != "deleted",
|
||||
)
|
||||
.count()
|
||||
)
|
||||
if status:
|
||||
query = query.filter(VoiceLibraryModel.status == status)
|
||||
return query.count()
|
||||
|
||||
@staticmethod
|
||||
def _model_to_entity(model: VoiceLibraryModel) -> VoiceLibraryItem:
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
"""预置音色配置 — Phase 3 CosyVoice 集成.
|
||||
|
||||
定义系统预置的 CosyVoice 音色列表,不存数据库,以配置方式提供。
|
||||
音色 ID 对应阿里云 CosyVoice 模型中的真实音色名。
|
||||
|
||||
参考: https://help.aliyun.com/zh/model-studio/cosyvoice-voice-list
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class PresetVoice:
|
||||
"""预置音色定义。
|
||||
|
||||
Attributes:
|
||||
voice_id: CosyVoice 模型音色名(如 longxiaochun)
|
||||
name: 中文展示名
|
||||
description: 音色描述
|
||||
gender: 性别(male/female)
|
||||
language: 语言代码(如 zh-CN)
|
||||
preview_url: 预览音频 URL(可选)
|
||||
tags: 标签列表
|
||||
"""
|
||||
|
||||
voice_id: str
|
||||
name: str
|
||||
description: str
|
||||
gender: str
|
||||
language: str = "zh-CN"
|
||||
preview_url: str = ""
|
||||
tags: list[str] | None = None
|
||||
|
||||
def to_dict(self) -> dict:
|
||||
"""序列化为字典。"""
|
||||
return {
|
||||
"voice_id": self.voice_id,
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"gender": self.gender,
|
||||
"language": self.language,
|
||||
"preview_url": self.preview_url,
|
||||
"tags": self.tags or [],
|
||||
}
|
||||
|
||||
|
||||
# 预置音色列表(阿里云 CosyVoice 真实可用音色)
|
||||
PRESET_VOICES: list[PresetVoice] = [
|
||||
PresetVoice(
|
||||
voice_id="longxiaochun",
|
||||
name="龙小淳",
|
||||
description="温柔女声,适合情感类内容",
|
||||
gender="female",
|
||||
language="zh-CN",
|
||||
tags=["温柔", "女声", "情感"],
|
||||
),
|
||||
PresetVoice(
|
||||
voice_id="longxiaoxia",
|
||||
name="龙小夏",
|
||||
description="知性女声,适合新闻播报",
|
||||
gender="female",
|
||||
language="zh-CN",
|
||||
tags=["知性", "女声", "播报"],
|
||||
),
|
||||
PresetVoice(
|
||||
voice_id="longxiaochen",
|
||||
name="龙小晨",
|
||||
description="磁性男声,适合有声书",
|
||||
gender="male",
|
||||
language="zh-CN",
|
||||
tags=["磁性", "男声", "有声书"],
|
||||
),
|
||||
PresetVoice(
|
||||
voice_id="longyue",
|
||||
name="龙悦",
|
||||
description="甜美女声,适合广告配音",
|
||||
gender="female",
|
||||
language="zh-CN",
|
||||
tags=["甜美", "女声", "广告"],
|
||||
),
|
||||
PresetVoice(
|
||||
voice_id="longshu",
|
||||
name="龙书",
|
||||
description="沉稳男声,适合教育讲解",
|
||||
gender="male",
|
||||
language="zh-CN",
|
||||
tags=["沉稳", "男声", "教育"],
|
||||
),
|
||||
PresetVoice(
|
||||
voice_id="longjing",
|
||||
name="龙静",
|
||||
description="优雅女声,适合纪录片解说",
|
||||
gender="female",
|
||||
language="zh-CN",
|
||||
tags=["优雅", "女声", "纪录片"],
|
||||
),
|
||||
PresetVoice(
|
||||
voice_id="longbo",
|
||||
name="龙博",
|
||||
description="浑厚男声,适合科技类内容",
|
||||
gender="male",
|
||||
language="zh-CN",
|
||||
tags=["浑厚", "男声", "科技"],
|
||||
),
|
||||
PresetVoice(
|
||||
voice_id="longtian",
|
||||
name="龙甜",
|
||||
description="活泼女声,适合短视频配音",
|
||||
gender="female",
|
||||
language="zh-CN",
|
||||
tags=["活泼", "女声", "短视频"],
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
def get_preset_voices() -> list[PresetVoice]:
|
||||
"""获取所有预置音色。"""
|
||||
return PRESET_VOICES
|
||||
|
||||
|
||||
def get_preset_voice_by_id(voice_id: str) -> PresetVoice | None:
|
||||
"""根据 voice_id 获取预置音色。"""
|
||||
for voice in PRESET_VOICES:
|
||||
if voice.voice_id == voice_id:
|
||||
return voice
|
||||
return None
|
||||
|
||||
|
||||
def is_preset_voice(voice_id: str) -> bool:
|
||||
"""判断是否为预置音色。"""
|
||||
return get_preset_voice_by_id(voice_id) is not None
|
||||
@@ -0,0 +1,187 @@
|
||||
"""预置音色配置单元测试 — Phase 3 CosyVoice 集成."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import pytest
|
||||
|
||||
from packages.domain.preset_voices import (
|
||||
PRESET_VOICES,
|
||||
PresetVoice,
|
||||
get_preset_voice_by_id,
|
||||
get_preset_voices,
|
||||
is_preset_voice,
|
||||
)
|
||||
|
||||
|
||||
class TestPresetVoice:
|
||||
"""测试 PresetVoice 数据类。"""
|
||||
|
||||
def test_preset_voice_fields(self) -> None:
|
||||
"""预置音色字段完整。"""
|
||||
voice = PresetVoice(
|
||||
voice_id="test_voice",
|
||||
name="测试音色",
|
||||
description="测试描述",
|
||||
gender="female",
|
||||
language="zh-CN",
|
||||
preview_url="https://example.com/preview.mp3",
|
||||
tags=["测试"],
|
||||
)
|
||||
|
||||
assert voice.voice_id == "test_voice"
|
||||
assert voice.name == "测试音色"
|
||||
assert voice.description == "测试描述"
|
||||
assert voice.gender == "female"
|
||||
assert voice.language == "zh-CN"
|
||||
assert voice.preview_url == "https://example.com/preview.mp3"
|
||||
assert voice.tags == ["测试"]
|
||||
|
||||
def test_preset_voice_defaults(self) -> None:
|
||||
"""预置音色默认值。"""
|
||||
voice = PresetVoice(
|
||||
voice_id="test",
|
||||
name="测试",
|
||||
description="描述",
|
||||
gender="male",
|
||||
)
|
||||
|
||||
assert voice.language == "zh-CN"
|
||||
assert voice.preview_url == ""
|
||||
assert voice.tags is None
|
||||
|
||||
def test_preset_voice_frozen(self) -> None:
|
||||
"""预置音色不可变。"""
|
||||
voice = PresetVoice(
|
||||
voice_id="test",
|
||||
name="测试",
|
||||
description="描述",
|
||||
gender="male",
|
||||
)
|
||||
|
||||
with pytest.raises(AttributeError):
|
||||
voice.name = "新名称"
|
||||
|
||||
def test_preset_voice_to_dict(self) -> None:
|
||||
"""序列化。"""
|
||||
voice = PresetVoice(
|
||||
voice_id="longxiaochun",
|
||||
name="龙小淳",
|
||||
description="温柔女声",
|
||||
gender="female",
|
||||
tags=["温柔", "女声"],
|
||||
)
|
||||
|
||||
result = voice.to_dict()
|
||||
|
||||
assert result["voice_id"] == "longxiaochun"
|
||||
assert result["name"] == "龙小淳"
|
||||
assert result["description"] == "温柔女声"
|
||||
assert result["gender"] == "female"
|
||||
assert result["language"] == "zh-CN"
|
||||
assert result["preview_url"] == ""
|
||||
assert result["tags"] == ["温柔", "女声"]
|
||||
|
||||
def test_preset_voice_to_dict_no_tags(self) -> None:
|
||||
"""tags 为 None 时序列化为空列表。"""
|
||||
voice = PresetVoice(
|
||||
voice_id="test",
|
||||
name="测试",
|
||||
description="描述",
|
||||
gender="male",
|
||||
)
|
||||
|
||||
result = voice.to_dict()
|
||||
assert result["tags"] == []
|
||||
|
||||
|
||||
class TestPresetVoicesConfig:
|
||||
"""测试预置音色配置列表。"""
|
||||
|
||||
def test_preset_voices_not_empty(self) -> None:
|
||||
"""预置音色列表不为空。"""
|
||||
assert len(PRESET_VOICES) >= 5
|
||||
|
||||
def test_preset_voices_has_8_voices(self) -> None:
|
||||
"""应有 8 个预置音色。"""
|
||||
assert len(PRESET_VOICES) == 8
|
||||
|
||||
def test_all_voices_have_required_fields(self) -> None:
|
||||
"""所有预置音色都有必填字段。"""
|
||||
for voice in PRESET_VOICES:
|
||||
assert voice.voice_id, f"voice_id 为空: {voice}"
|
||||
assert voice.name, f"name 为空: {voice}"
|
||||
assert voice.description, f"description 为空: {voice}"
|
||||
assert voice.gender in ("male", "female"), f"gender 无效: {voice}"
|
||||
assert voice.language == "zh-CN", f"language 应为 zh-CN: {voice}"
|
||||
|
||||
def test_all_voices_have_unique_ids(self) -> None:
|
||||
"""所有预置音色 ID 唯一。"""
|
||||
ids = [v.voice_id for v in PRESET_VOICES]
|
||||
assert len(ids) == len(set(ids)), "存在重复的 voice_id"
|
||||
|
||||
def test_all_voices_have_unique_names(self) -> None:
|
||||
"""所有预置音色名称唯一。"""
|
||||
names = [v.name for v in PRESET_VOICES]
|
||||
assert len(names) == len(set(names)), "存在重复的 name"
|
||||
|
||||
def test_cosyvoice_voice_ids(self) -> None:
|
||||
"""音色 ID 应为 CosyVoice 真实可用的音色名。"""
|
||||
expected_ids = {
|
||||
"longxiaochun",
|
||||
"longxiaoxia",
|
||||
"longxiaochen",
|
||||
"longyue",
|
||||
"longshu",
|
||||
"longjing",
|
||||
"longbo",
|
||||
"longtian",
|
||||
}
|
||||
actual_ids = {v.voice_id for v in PRESET_VOICES}
|
||||
assert actual_ids == expected_ids
|
||||
|
||||
def test_gender_distribution(self) -> None:
|
||||
"""男女音色分布合理。"""
|
||||
male_count = sum(1 for v in PRESET_VOICES if v.gender == "male")
|
||||
female_count = sum(1 for v in PRESET_VOICES if v.gender == "female")
|
||||
assert male_count == 3
|
||||
assert female_count == 5
|
||||
|
||||
def test_all_have_tags(self) -> None:
|
||||
"""所有预置音色都有标签。"""
|
||||
for voice in PRESET_VOICES:
|
||||
assert voice.tags is not None, f"tags 为 None: {voice.name}"
|
||||
assert len(voice.tags) >= 2, f"标签太少: {voice.name}"
|
||||
|
||||
|
||||
class TestPresetVoiceHelpers:
|
||||
"""测试辅助函数。"""
|
||||
|
||||
def test_get_preset_voices(self) -> None:
|
||||
"""get_preset_voices 返回完整列表。"""
|
||||
voices = get_preset_voices()
|
||||
assert voices == PRESET_VOICES
|
||||
assert len(voices) == 8
|
||||
|
||||
def test_get_preset_voice_by_id_found(self) -> None:
|
||||
"""按 ID 查找存在的音色。"""
|
||||
voice = get_preset_voice_by_id("longxiaochun")
|
||||
assert voice is not None
|
||||
assert voice.name == "龙小淳"
|
||||
assert voice.voice_id == "longxiaochun"
|
||||
|
||||
def test_get_preset_voice_by_id_not_found(self) -> None:
|
||||
"""按 ID 查找不存在的音色。"""
|
||||
voice = get_preset_voice_by_id("nonexistent_voice")
|
||||
assert voice is None
|
||||
|
||||
def test_is_preset_voice_true(self) -> None:
|
||||
"""判断预置音色返回 True。"""
|
||||
assert is_preset_voice("longxiaochun") is True
|
||||
assert is_preset_voice("longxiaoxia") is True
|
||||
assert is_preset_voice("longbo") is True
|
||||
|
||||
def test_is_preset_voice_false(self) -> None:
|
||||
"""判断非预置音色返回 False。"""
|
||||
assert is_preset_voice("nonexistent") is False
|
||||
assert is_preset_voice("") is False
|
||||
assert is_preset_voice("user_custom_voice") is False
|
||||
Reference in New Issue
Block a user