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
223 lines
6.0 KiB
Python
Executable File
223 lines
6.0 KiB
Python
Executable File
"""配音引擎音色预设.
|
||
|
||
与 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]
|