|
7 | 7 | Gemini-bound by virtue of speaking Gemini's wire format (the |
8 | 8 | `gemini_tts_worker` HTTP call and the Gemini Live `speech_config` setup). |
9 | 9 |
|
| 10 | +音色 ID、展示性别和默认值优先读取自 config/api_providers.json 的 |
| 11 | +native_tts_voice_providers.gemini,避免修改音色清单要动 Python 代码。 |
| 12 | +fallback 常量是 PR #1290 之前的硬编码目录的副本,仅在 JSON 加载失败时兜底 |
| 13 | +—— 此时 provider 仍必须留在 registry 里,否则 |
| 14 | +`resolve_native_voice_for_routing("gemini", ...)` 会判 native=False, |
| 15 | +`core._has_custom_tts()` 把内置音色当 custom,最终把 Puck/Leda 也路由到 |
| 16 | +cosyvoice_vc_tts_worker,比"丢失目录元数据"更隐蔽的 routing 回归。 |
| 17 | +
|
10 | 18 | Voice list reference: https://ai.google.dev/gemini-api/docs/speech-generation |
11 | 19 | """ |
12 | 20 |
|
| 21 | +from utils.api_config_loader import get_native_tts_voice_provider_config |
13 | 22 | from utils.native_voice_registry import ( |
14 | 23 | NativeVoiceProvider, |
15 | 24 | register_provider, |
16 | 25 | ) |
17 | 26 |
|
18 | 27 | GEMINI_TTS_MODEL = "gemini-2.5-flash-preview-tts" |
19 | | -GEMINI_TTS_DEFAULT_VOICE = "Leda" |
20 | | -GEMINI_TTS_DEFAULT_MALE_VOICE = "Puck" |
21 | 28 |
|
22 | | -GEMINI_TTS_VOICE_GENDERS: dict[str, str] = { |
| 29 | +FALLBACK_GEMINI_TTS_DEFAULT_VOICE = "Leda" |
| 30 | +FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE = "Puck" |
| 31 | + |
| 32 | +# 与 api_providers.json 的 native_tts_voice_providers.gemini.voices 保持 |
| 33 | +# 同形;config 是权威源,这份是 JSON 加载失败时的兜底,保证 provider 始终 |
| 34 | +# 注册成功、routing 不退化到 cosyvoice。两边漂移的代价仅仅是"新版 JSON |
| 35 | +# 加的音色在 config 缺失时不可见",比 routing 走错路要轻。 |
| 36 | +_FALLBACK_GEMINI_TTS_VOICE_GENDERS: dict[str, str] = { |
23 | 37 | "Achernar": "Female", |
24 | 38 | "Achird": "Male", |
25 | 39 | "Algenib": "Male", |
|
52 | 66 | "Zubenelgenubi": "Male", |
53 | 67 | } |
54 | 68 |
|
55 | | -_GEMINI_TTS_VOICE_ALIASES: dict[str, str] = { |
56 | | - "male": GEMINI_TTS_DEFAULT_MALE_VOICE, |
57 | | - "man": GEMINI_TTS_DEFAULT_MALE_VOICE, |
58 | | - "masculine": GEMINI_TTS_DEFAULT_MALE_VOICE, |
59 | | - "男": GEMINI_TTS_DEFAULT_MALE_VOICE, |
60 | | - "男声": GEMINI_TTS_DEFAULT_MALE_VOICE, |
61 | | - "中文男": GEMINI_TTS_DEFAULT_MALE_VOICE, |
62 | | - "female": GEMINI_TTS_DEFAULT_VOICE, |
63 | | - "woman": GEMINI_TTS_DEFAULT_VOICE, |
64 | | - "feminine": GEMINI_TTS_DEFAULT_VOICE, |
65 | | - "女": GEMINI_TTS_DEFAULT_VOICE, |
66 | | - "女声": GEMINI_TTS_DEFAULT_VOICE, |
67 | | - "中文女": GEMINI_TTS_DEFAULT_VOICE, |
| 69 | +_FALLBACK_GEMINI_TTS_VOICE_ALIASES: dict[str, str] = { |
| 70 | + "male": FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE, |
| 71 | + "man": FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE, |
| 72 | + "masculine": FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE, |
| 73 | + "男": FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE, |
| 74 | + "男声": FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE, |
| 75 | + "中文男": FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE, |
| 76 | + "female": FALLBACK_GEMINI_TTS_DEFAULT_VOICE, |
| 77 | + "woman": FALLBACK_GEMINI_TTS_DEFAULT_VOICE, |
| 78 | + "feminine": FALLBACK_GEMINI_TTS_DEFAULT_VOICE, |
| 79 | + "女": FALLBACK_GEMINI_TTS_DEFAULT_VOICE, |
| 80 | + "女声": FALLBACK_GEMINI_TTS_DEFAULT_VOICE, |
| 81 | + "中文女": FALLBACK_GEMINI_TTS_DEFAULT_VOICE, |
68 | 82 | } |
69 | 83 |
|
70 | | -GEMINI_PROVIDER = NativeVoiceProvider( |
71 | | - key="gemini", |
72 | | - catalog=GEMINI_TTS_VOICE_GENDERS, |
73 | | - aliases=_GEMINI_TTS_VOICE_ALIASES, |
74 | | - default_voice=GEMINI_TTS_DEFAULT_VOICE, |
75 | | - default_male_voice=GEMINI_TTS_DEFAULT_MALE_VOICE, |
76 | | - catalog_prefix="Gemini", |
| 84 | + |
| 85 | +def _load_provider_config() -> dict: |
| 86 | + return get_native_tts_voice_provider_config("gemini") |
| 87 | + |
| 88 | + |
| 89 | +_CFG = _load_provider_config() |
| 90 | + |
| 91 | +GEMINI_TTS_VOICE_GENDERS: dict[str, str] = ( |
| 92 | + _CFG.get("voices") or _FALLBACK_GEMINI_TTS_VOICE_GENDERS |
| 93 | +) |
| 94 | +GEMINI_TTS_DEFAULT_VOICE = ( |
| 95 | + _CFG.get("default_voice") or FALLBACK_GEMINI_TTS_DEFAULT_VOICE |
77 | 96 | ) |
| 97 | +GEMINI_TTS_DEFAULT_MALE_VOICE = ( |
| 98 | + _CFG.get("default_male_voice") or FALLBACK_GEMINI_TTS_DEFAULT_MALE_VOICE |
| 99 | +) |
| 100 | + |
| 101 | + |
| 102 | +def _build_aliases(configured: dict[str, str]) -> dict[str, str]: |
| 103 | + """Casefold alias keys so NativeVoiceProvider.normalize 的 casefold 查表能命中。 |
| 104 | + 与 stepfun_tts_voices._build_aliases 的差别:Gemini 的 catalog value 是性别 |
| 105 | + (Female/Male) 而非展示名,不应把它当 alias 注入回去。""" |
| 106 | + return { |
| 107 | + alias.casefold(): voice_id |
| 108 | + for alias, voice_id in configured.items() |
| 109 | + if alias and voice_id |
| 110 | + } |
| 111 | + |
| 112 | + |
| 113 | +def _create_provider() -> NativeVoiceProvider: |
| 114 | + """Always succeed — provider 必须留在 registry 里,否则下游 routing 会 |
| 115 | + 把内置 Gemini 音色误判为 custom。catalog/默认值上面已经走过 config → |
| 116 | + fallback 的 OR 链,到这里保证非空。""" |
| 117 | + aliases_source = _CFG.get("aliases") or _FALLBACK_GEMINI_TTS_VOICE_ALIASES |
| 118 | + return NativeVoiceProvider( |
| 119 | + key="gemini", |
| 120 | + catalog=GEMINI_TTS_VOICE_GENDERS, |
| 121 | + aliases=_build_aliases(aliases_source), |
| 122 | + default_voice=GEMINI_TTS_DEFAULT_VOICE, |
| 123 | + default_male_voice=GEMINI_TTS_DEFAULT_MALE_VOICE, |
| 124 | + catalog_prefix=_CFG.get("catalog_prefix") or "Gemini", |
| 125 | + catalog_value_is_display_name=bool( |
| 126 | + _CFG.get("catalog_value_is_display_name", False) |
| 127 | + ), |
| 128 | + ) |
| 129 | + |
78 | 130 |
|
| 131 | +GEMINI_PROVIDER = _create_provider() |
79 | 132 | register_provider(GEMINI_PROVIDER) |
80 | 133 |
|
81 | 134 |
|
|
0 commit comments