Files
xiaoxia-saas/packages/domain/voice_presets.py
T
xiaoxia c840f37a44
CI/CD Pipeline / Validate Code Quality And Tests (push) Successful in 33s
CI/CD Pipeline / Frontend Lint (push) Successful in 1m12s
CI/CD Pipeline / Build Production Runtime Images (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 / Integration Tests (push) Failing after 1m8s
CI/CD Pipeline / Unit Tests (push) Successful in 2m52s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (push) Has been cancelled
CI/CD Pipeline / Staging E2E Tests (push) Has been cancelled
CI/CD Pipeline / Staging API Integration Tests (push) Has been cancelled
feat: TTS文字转语音配音引擎 (#295)
2026-07-14 10:21:49 +08:00

223 lines
6.0 KiB
Python
Executable File
Raw 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.
"""配音引擎音色预设.
与 CosyVoice 的 preset_voices 区分:
- preset_voices.py: CosyVoice 真实音色(阿里云)
- voice_presets.py: 配音引擎通用音色预设(含 mock/后续接入的真实 TTS)
"""
from __future__ import annotations
import sys
from dataclasses import dataclass
if sys.version_info >= (3, 11):
from enum import StrEnum
else:
from enum import Enum
class StrEnum(str, Enum):
pass
class VoiceGender(StrEnum):
"""音色性别."""
MALE = "male"
FEMALE = "female"
CHILD = "child"
class VoiceStyle(StrEnum):
"""音色风格."""
STABLE = "stable" # 沉稳
LIVELY = "lively" # 活泼
CUSTOMER_SERVICE = "customer_service" # 客服
NARRATION = "narration" # 旁白
NEWS = "news" # 新闻
STORY = "story" # 故事
@dataclass(slots=True)
class VoicePreset:
"""音色预设.
Attributes:
voice_id: 音色唯一标识
name: 音色名称
gender: 性别
style: 风格
description: 描述
provider: 供应商(mock/aliyun/xunfei
provider_voice_id: 供应商侧音色 ID
default_speed: 默认语速
default_pitch: 默认语调
sample_rate: 采样率
language: 语言
"""
voice_id: str
name: str
gender: VoiceGender = VoiceGender.FEMALE
style: VoiceStyle = VoiceStyle.NARRATION
description: str = ""
provider: str = "mock"
provider_voice_id: str = ""
default_speed: float = 1.0
default_pitch: float = 0.0
sample_rate: int = 22050
language: str = "zh-CN"
# ─── Mock 音色预设列表 ──────────────────────────────────────
MOCK_VOICES: list[VoicePreset] = [
VoicePreset(
voice_id="female_warm",
name="温暖女声",
gender=VoiceGender.FEMALE,
style=VoiceStyle.NARRATION,
description="温柔温暖的女声,适合情感类、生活类视频",
provider="mock",
provider_voice_id="sine_220",
default_speed=1.0,
default_pitch=0.0,
sample_rate=22050,
language="zh-CN",
),
VoicePreset(
voice_id="male_stable",
name="沉稳男声",
gender=VoiceGender.MALE,
style=VoiceStyle.STABLE,
description="沉稳厚重的男声,适合商务、知识类视频",
provider="mock",
provider_voice_id="sine_110",
default_speed=0.9,
default_pitch=0.0,
sample_rate=22050,
language="zh-CN",
),
VoicePreset(
voice_id="female_lively",
name="活泼女声",
gender=VoiceGender.FEMALE,
style=VoiceStyle.LIVELY,
description="明亮活泼的女声,适合vlog、美食、旅行类视频",
provider="mock",
provider_voice_id="sine_280",
default_speed=1.2,
default_pitch=2.0,
sample_rate=22050,
language="zh-CN",
),
VoicePreset(
voice_id="child_cute",
name="可爱童声",
gender=VoiceGender.CHILD,
style=VoiceStyle.STORY,
description="清脆可爱的童声,适合儿童教育、动画类视频",
provider="mock",
provider_voice_id="sine_380",
default_speed=1.0,
default_pitch=4.0,
sample_rate=22050,
language="zh-CN",
),
VoicePreset(
voice_id="female_service",
name="客服女声",
gender=VoiceGender.FEMALE,
style=VoiceStyle.CUSTOMER_SERVICE,
description="专业清晰的客服女声,适合产品介绍、教程类视频",
provider="mock",
provider_voice_id="sine_250",
default_speed=1.0,
default_pitch=1.0,
sample_rate=22050,
language="zh-CN",
),
VoicePreset(
voice_id="male_news",
name="新闻男声",
gender=VoiceGender.MALE,
style=VoiceStyle.NEWS,
description="字正腔圆的新闻播报声,适合资讯、时政类视频",
provider="mock",
provider_voice_id="sine_140",
default_speed=1.0,
default_pitch=0.0,
sample_rate=22050,
language="zh-CN",
),
VoicePreset(
voice_id="female_soft",
name="轻柔女声",
gender=VoiceGender.FEMALE,
style=VoiceStyle.STORY,
description="轻柔舒缓的女声,适合睡前故事、冥想类视频",
provider="mock",
provider_voice_id="sine_180",
default_speed=0.8,
default_pitch=0.0,
sample_rate=22050,
language="zh-CN",
),
VoicePreset(
voice_id="male_magnetic",
name="磁性男声",
gender=VoiceGender.MALE,
style=VoiceStyle.STORY,
description="低沉磁性的男声,适合电影解说、读书类视频",
provider="mock",
provider_voice_id="sine_90",
default_speed=0.85,
default_pitch=-2.0,
sample_rate=22050,
language="zh-CN",
),
]
# voice_id → VoicePreset
_MOCK_VOICE_MAP: dict[str, VoicePreset] = {v.voice_id: v for v in MOCK_VOICES}
def get_voice(voice_id: str, *, provider: str = "mock") -> VoicePreset | None:
"""根据 voice_id 获取音色预设."""
if provider == "mock":
return _MOCK_VOICE_MAP.get(voice_id)
return None
def list_voices(
*,
gender: str | None = None,
style: str | None = None,
provider: str | None = None,
keyword: str | None = None,
) -> list[VoicePreset]:
"""按条件筛选音色列表."""
# 目前只有 mock 音色
result = list(MOCK_VOICES)
if provider and provider != "mock":
return []
if gender:
result = [v for v in result if v.gender.value == gender]
if style:
result = [v for v in result if v.style.value == style]
if keyword:
kw = keyword.lower()
result = [v for v in result if kw in v.name.lower() or kw in v.description.lower() or kw in v.voice_id.lower()]
return result
def get_default_voice() -> VoicePreset:
"""获取默认音色."""
return MOCK_VOICES[0]