feat: Integrate Aliyun TTS backend with dynamic speaker validation and listing, and adjust toast notification position.
Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -27,4 +27,4 @@ qwen3-tts-frontend/dist/
|
|||||||
qwen3-tts-frontend/.env
|
qwen3-tts-frontend/.env
|
||||||
qwen3-tts-frontend/.env.local
|
qwen3-tts-frontend/.env.local
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
样本.mp3
|
样本.mp3aliyun.md
|
||||||
|
|||||||
@@ -309,7 +309,7 @@ async def create_custom_voice_job(
|
|||||||
try:
|
try:
|
||||||
validate_text_length(req_data.text)
|
validate_text_length(req_data.text)
|
||||||
language = validate_language(req_data.language)
|
language = validate_language(req_data.language)
|
||||||
speaker = validate_speaker(req_data.speaker)
|
speaker = validate_speaker(req_data.speaker, backend_type)
|
||||||
|
|
||||||
params = validate_generation_params({
|
params = validate_generation_params({
|
||||||
'max_new_tokens': req_data.max_new_tokens,
|
'max_new_tokens': req_data.max_new_tokens,
|
||||||
@@ -581,8 +581,8 @@ async def list_models(request: Request):
|
|||||||
|
|
||||||
@router.get("/speakers")
|
@router.get("/speakers")
|
||||||
@limiter.limit("30/minute")
|
@limiter.limit("30/minute")
|
||||||
async def list_speakers(request: Request):
|
async def list_speakers(request: Request, backend: Optional[str] = "local"):
|
||||||
return get_supported_speakers()
|
return get_supported_speakers(backend)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/languages")
|
@router.get("/languages")
|
||||||
|
|||||||
@@ -387,14 +387,16 @@ class AliyunTTSBackend(TTSBackend):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _map_speaker(local_speaker: str) -> str:
|
def _map_speaker(local_speaker: str) -> str:
|
||||||
mapping = {
|
mapping = {
|
||||||
"Vivian": "Cherry",
|
"Ono_Anna": "Ono Anna",
|
||||||
"Serena": "Lili",
|
|
||||||
"Uncle_Fu": "Longxiaochun",
|
|
||||||
"Dylan": "Longxiaochun",
|
|
||||||
"Female": "Cherry",
|
"Female": "Cherry",
|
||||||
"Male": "Longxiaochun"
|
"Male": "Ethan"
|
||||||
}
|
}
|
||||||
return mapping.get(local_speaker, "Cherry")
|
|
||||||
|
mapped = mapping.get(local_speaker)
|
||||||
|
if mapped:
|
||||||
|
return mapped
|
||||||
|
|
||||||
|
return local_speaker
|
||||||
|
|
||||||
|
|
||||||
class TTSServiceFactory:
|
class TTSServiceFactory:
|
||||||
|
|||||||
@@ -23,6 +23,26 @@ SPEAKER_DESCRIPTIONS = {
|
|||||||
"Sohee": "Female, soft and melodious"
|
"Sohee": "Female, soft and melodious"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ALIYUN_SPEAKERS = [
|
||||||
|
"Vivian", "Serena", "Dylan", "Eric",
|
||||||
|
"Ryan", "Aiden", "Ono_Anna", "Sohee"
|
||||||
|
]
|
||||||
|
|
||||||
|
ALIYUN_SPEAKER_DESCRIPTIONS = {
|
||||||
|
"Vivian": "Female, cute and lively (十三 - 拽拽的、可爱的小暴躁)",
|
||||||
|
"Serena": "Female, gentle and warm (苏瑶 - 温柔小姐姐)",
|
||||||
|
"Dylan": "Male, young and energetic (北京-晓东 - 北京胡同里长大的少年)",
|
||||||
|
"Eric": "Male, calm and steady (四川-程川 - 跳脱市井的四川成都男子)",
|
||||||
|
"Ryan": "Male, friendly and dramatic (甜茶 - 节奏拉满,戏感炸裂)",
|
||||||
|
"Aiden": "Male, deep and resonant (艾登 - 精通厨艺的美语大男孩)",
|
||||||
|
"Ono_Anna": "Female, cute and playful (小野杏 - 鬼灵精怪的青梅竹马)",
|
||||||
|
"Sohee": "Female, soft and melodious (素熙 - 温柔开朗的韩国欧尼)"
|
||||||
|
}
|
||||||
|
|
||||||
|
LOCAL_SPEAKERS = SUPPORTED_SPEAKERS.copy()
|
||||||
|
|
||||||
|
LOCAL_SPEAKER_DESCRIPTIONS = SPEAKER_DESCRIPTIONS.copy()
|
||||||
|
|
||||||
|
|
||||||
def validate_language(language: str) -> str:
|
def validate_language(language: str) -> str:
|
||||||
normalized = language.strip()
|
normalized = language.strip()
|
||||||
@@ -37,16 +57,21 @@ def validate_language(language: str) -> str:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def validate_speaker(speaker: str) -> str:
|
def validate_speaker(speaker: str, backend: str = "local") -> str:
|
||||||
normalized = speaker.strip()
|
normalized = speaker.strip()
|
||||||
|
|
||||||
for supported in SUPPORTED_SPEAKERS:
|
if backend == "aliyun":
|
||||||
|
speaker_list = ALIYUN_SPEAKERS
|
||||||
|
else:
|
||||||
|
speaker_list = LOCAL_SPEAKERS
|
||||||
|
|
||||||
|
for supported in speaker_list:
|
||||||
if normalized.lower() == supported.lower():
|
if normalized.lower() == supported.lower():
|
||||||
return supported
|
return supported
|
||||||
|
|
||||||
raise ValueError(
|
raise ValueError(
|
||||||
f"Unsupported speaker: {speaker}. "
|
f"Unsupported speaker: {speaker} for backend '{backend}'. "
|
||||||
f"Supported speakers: {', '.join(SUPPORTED_SPEAKERS)}"
|
f"Supported speakers: {', '.join(speaker_list)}"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@@ -92,11 +117,18 @@ def get_supported_languages() -> List[str]:
|
|||||||
return SUPPORTED_LANGUAGES.copy()
|
return SUPPORTED_LANGUAGES.copy()
|
||||||
|
|
||||||
|
|
||||||
def get_supported_speakers() -> List[dict]:
|
def get_supported_speakers(backend: str = "local") -> List[dict]:
|
||||||
|
if backend == "aliyun":
|
||||||
|
speakers = ALIYUN_SPEAKERS
|
||||||
|
descriptions = ALIYUN_SPEAKER_DESCRIPTIONS
|
||||||
|
else:
|
||||||
|
speakers = LOCAL_SPEAKERS
|
||||||
|
descriptions = LOCAL_SPEAKER_DESCRIPTIONS
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
"name": speaker,
|
"name": speaker,
|
||||||
"description": SPEAKER_DESCRIPTIONS.get(speaker, "")
|
"description": descriptions.get(speaker, "")
|
||||||
}
|
}
|
||||||
for speaker in SUPPORTED_SPEAKERS
|
for speaker in speakers
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ function App() {
|
|||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<AuthProvider>
|
<AuthProvider>
|
||||||
<UserPreferencesProvider>
|
<UserPreferencesProvider>
|
||||||
<Toaster position="top-right" />
|
<Toaster position="top-center" offset="16px" />
|
||||||
<Suspense fallback={<LoadingScreen />}>
|
<Suspense fallback={<LoadingScreen />}>
|
||||||
<Routes>
|
<Routes>
|
||||||
<Route
|
<Route
|
||||||
|
|||||||
Reference in New Issue
Block a user