diff --git a/app/services/voice.py b/app/services/voice.py index bda672b..e6c94f7 100644 --- a/app/services/voice.py +++ b/app/services/voice.py @@ -6,6 +6,7 @@ import edge_tts import asyncio import requests import uuid +from functools import lru_cache from loguru import logger from typing import List, Union, Tuple from datetime import datetime @@ -282,7 +283,7 @@ Gender: Male Name: en-AU-NatashaNeural Gender: Female -Name: en-AU-WilliamNeural +Name: en-AU-WilliamMultilingualNeural Gender: Male Name: en-CA-ClaraNeural @@ -369,21 +370,33 @@ Gender: Female Name: en-US-AndrewNeural Gender: Male +Name: en-US-AndrewMultilingualNeural +Gender: Male + Name: en-US-AriaNeural Gender: Female Name: en-US-AvaNeural Gender: Female +Name: en-US-AvaMultilingualNeural +Gender: Female + Name: en-US-BrianNeural Gender: Male +Name: en-US-BrianMultilingualNeural +Gender: Male + Name: en-US-ChristopherNeural Gender: Male Name: en-US-EmmaNeural Gender: Female +Name: en-US-EmmaMultilingualNeural +Gender: Female + Name: en-US-EricNeural Gender: Male @@ -666,12 +679,24 @@ Gender: Male Name: it-IT-ElsaNeural Gender: Female -Name: it-IT-GiuseppeNeural +Name: it-IT-GiuseppeMultilingualNeural Gender: Male Name: it-IT-IsabellaNeural Gender: Female +Name: iu-Cans-CA-SiqiniqNeural +Gender: Female + +Name: iu-Cans-CA-TaqqiqNeural +Gender: Male + +Name: iu-Latn-CA-SiqiniqNeural +Gender: Female + +Name: iu-Latn-CA-TaqqiqNeural +Gender: Male + Name: ja-JP-KeitaNeural Gender: Male @@ -708,7 +733,7 @@ Gender: Male Name: kn-IN-SapnaNeural Gender: Female -Name: ko-KR-HyunsuNeural +Name: ko-KR-HyunsuMultilingualNeural Gender: Male Name: ko-KR-InJoonNeural @@ -822,7 +847,7 @@ Gender: Male Name: pt-BR-FranciscaNeural Gender: Female -Name: pt-BR-ThalitaNeural +Name: pt-BR-ThalitaMultilingualNeural Gender: Female Name: pt-PT-DuarteNeural @@ -1306,6 +1331,52 @@ def get_edge_tts_proxy() -> str | None: return proxy_url or None +def _run_async_safely(coro_func, *args, **kwargs): + """在同步代码里安全运行异步 edge_tts 调用。""" + def run_in_new_loop(): + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + try: + return loop.run_until_complete(coro_func(*args, **kwargs)) + finally: + loop.close() + asyncio.set_event_loop(None) + + try: + asyncio.get_running_loop() + except RuntimeError: + return run_in_new_loop() + + import concurrent.futures + with concurrent.futures.ThreadPoolExecutor(max_workers=1) as executor: + return executor.submit(run_in_new_loop).result() + + +@lru_cache(maxsize=8) +def _get_all_edge_voices_cached(proxy: str | None) -> list[str]: + async def _list_voices(): + return await edge_tts.list_voices(proxy=proxy) + + voices = [] + for item in _run_async_safely(_list_voices): + name = item.get("ShortName", "").strip() + gender = item.get("Gender", "").strip() + if name and gender: + voices.append(f"{name}-{gender}") + + voices.sort() + return voices + + +def get_all_edge_voices() -> list[str]: + """获取 Edge TTS 当前支持的全部语言和音色,失败时回退到内置列表。""" + try: + return _get_all_edge_voices_cached(get_edge_tts_proxy()) + except Exception as e: + logger.warning(f"获取 Edge TTS 在线音色列表失败,使用内置音色列表: {e}") + return [v for v in get_all_azure_voices(filter_locals=[]) if "-V2" not in v] + + def azure_tts_v1( text: str, voice_name: str, voice_rate: float, voice_pitch: float, voice_file: str ) -> Union[SubMaker, None]: diff --git a/docs/voice-list.txt b/docs/voice-list.txt index 4672117..b22486d 100644 --- a/docs/voice-list.txt +++ b/docs/voice-list.txt @@ -199,7 +199,7 @@ Gender: Male Name: en-AU-NatashaNeural Gender: Female -Name: en-AU-WilliamNeural +Name: en-AU-WilliamMultilingualNeural Gender: Male Name: en-CA-ClaraNeural @@ -286,21 +286,33 @@ Gender: Female Name: en-US-AndrewNeural Gender: Male +Name: en-US-AndrewMultilingualNeural +Gender: Male + Name: en-US-AriaNeural Gender: Female Name: en-US-AvaNeural Gender: Female +Name: en-US-AvaMultilingualNeural +Gender: Female + Name: en-US-BrianNeural Gender: Male +Name: en-US-BrianMultilingualNeural +Gender: Male + Name: en-US-ChristopherNeural Gender: Male Name: en-US-EmmaNeural Gender: Female +Name: en-US-EmmaMultilingualNeural +Gender: Female + Name: en-US-EricNeural Gender: Male @@ -583,12 +595,24 @@ Gender: Male Name: it-IT-ElsaNeural Gender: Female -Name: it-IT-GiuseppeNeural +Name: it-IT-GiuseppeMultilingualNeural Gender: Male Name: it-IT-IsabellaNeural Gender: Female +Name: iu-Cans-CA-SiqiniqNeural +Gender: Female + +Name: iu-Cans-CA-TaqqiqNeural +Gender: Male + +Name: iu-Latn-CA-SiqiniqNeural +Gender: Female + +Name: iu-Latn-CA-TaqqiqNeural +Gender: Male + Name: ja-JP-KeitaNeural Gender: Male @@ -625,7 +649,7 @@ Gender: Male Name: kn-IN-SapnaNeural Gender: Female -Name: ko-KR-HyunsuNeural +Name: ko-KR-HyunsuMultilingualNeural Gender: Male Name: ko-KR-InJoonNeural @@ -739,7 +763,7 @@ Gender: Male Name: pt-BR-FranciscaNeural Gender: Female -Name: pt-BR-ThalitaNeural +Name: pt-BR-ThalitaMultilingualNeural Gender: Female Name: pt-PT-DuarteNeural diff --git a/webui/components/audio_settings.py b/webui/components/audio_settings.py index ed86698..ce4aa8e 100644 --- a/webui/components/audio_settings.py +++ b/webui/components/audio_settings.py @@ -163,12 +163,8 @@ def render_tts_settings(tr): def render_edge_tts_settings(tr): """渲染 Edge TTS 引擎设置""" - # 获取支持的语音列表 - support_locales = ["zh-CN", "en-US"] - all_voices = voice.get_all_azure_voices(filter_locals=support_locales) - - # 只保留标准版本的语音(Edge TTS专用,不包含V2) - edge_voices = [v for v in all_voices if "-V2" not in v] + # 获取 Edge TTS 支持的全部语言和音色 + edge_voices = voice.get_all_edge_voices() # 创建友好的显示名称 friendly_names = {} @@ -189,7 +185,7 @@ def render_edge_tts_settings(tr): # 如果没找到匹配的,使用第一个 saved_voice_name = edge_voices[0] if edge_voices else "" - # 音色选择下拉框(Edge TTS音色相对较少,保留下拉框) + # 音色选择下拉框 selected_friendly_name = st.selectbox( "音色选择", options=list(friendly_names.values()), @@ -204,24 +200,12 @@ def render_edge_tts_settings(tr): # 显示音色信息 with st.expander("💡 Edge TTS 音色说明", expanded=False): - st.write("**中文音色:**") - zh_voices = [v for v in edge_voices if v.startswith("zh-CN")] - for v in zh_voices: + st.write(f"已加载 {len(edge_voices)} 个音色") + for v in edge_voices: gender = "女声" if "Female" in v else "男声" - name = v.replace("-Female", "").replace("-Male", "").replace("zh-CN-", "").replace("Neural", "") + name = v.replace("-Female", "").replace("-Male", "").replace("Neural", "") st.write(f"• {name} ({gender})") - st.write("") - st.write("**英文音色:**") - en_voices = [v for v in edge_voices if v.startswith("en-US")][:5] # 只显示前5个 - for v in en_voices: - gender = "女声" if "Female" in v else "男声" - name = v.replace("-Female", "").replace("-Male", "").replace("en-US-", "").replace("Neural", "") - st.write(f"• {name} ({gender})") - - if len([v for v in edge_voices if v.startswith("en-US")]) > 5: - st.write("• ... 更多英文音色") - config.ui["edge_voice_name"] = voice_name config.ui["voice_name"] = voice_name # 兼容性