Skip to content

Commit baa6088

Browse files
wehosHongzhi Wenclaude
authored
feat: 海外免费支持 Gemini 全量音色 + yui/default (#1643)
* feat: 海外免费支持 Gemini 全量音色 + yui/default 国外玩家(lanlan.app 免费路由,Gemini 后端)选免费 API 时,音色列表展示 Gemini 全量原生音色,并把 yui(初始/默认)与 default(=Leda) 两项置顶。 核心机制 - 新增 free_intl provider(inherits gemini + 加 yui;default_voice=yui, 别名 default→Leda)。api_config_loader 的 inherits 改为对 voices/aliases 深合并,子配置只需增量声明。 - registry 按 host 把 free + *.lanlan.app 重映射到 free_intl(集中在 native_voice_registry,cross-cutting 文件不加 if host==)。校验、UI 目录、 路由、worker、预览全部走重映射;validate 用 is_saveable_native_voice 兼顾切线路时阶跃音色不被误清。 - 退役旧的 lanlan.app 强制 Leda 屏蔽逻辑(should_block_free_*)。 下发链路 - URL 统一到 www.lanlan.app(去掉把 /tts 降级到裸 lanlan.app 的 replace)。 - TTS streaming 透传真实 voice_id(去掉客户端硬覆盖 Leda),预览同步。 - core/realtime 海外建 session 时发 language_code,与 TTS server 对偶; language_code 映射表/取值收敛到 utils.language_utils 供两路共用。 其它 - 国内免费列表 yui_cn 置顶。 - yui 角色卡海外默认绑定 "yui"(国内仍绑 free_voices 阶跃音色)。 - 前端 voice_clone.js 与 character_card_manager.js 两个渲染器都渲染 pinned_voices(按 i18n_key 本地化)。 - i18n 8 语言新增 voice.freeVoice.yui / default;顺带补全缺失的 character.aiCompanionExpand / aiCompanionDragHint。 - 更新/新增相关单测。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: 海外免费置顶 pin 撞名时隐藏(Codex P2) 用户已注册/克隆了 ID 为 yui / Leda 的音色时,runtime 路由按撞名优先走克隆 路径、不再当 native(resolve_for_routing 的 collision 分支),此时置顶 pin 点了也到不了 Gemini。/voices 构造 pin 时按 voice_id_exists_in_any_storage 过滤掉撞名项,避免误导。加回归测试。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: 海外 YUI 默认音色判定 + 下拉重复值多选高亮(review) CX-D(config_manager):ensure_default_yui_voice_for_free_api 之前只看 raw core_cfg 的 CORE_URL,而 update_core_config 传进来的是保存前的原始值(仍是 lanlan.tech,区域改写在 get_core_config 才发生),导致海外用户的默认 YUI 卡 被绑成国内 voice-tone-* 而非 "yui"。改为 URL 命中 lanlan.app 走快路、否则用 _check_non_mainland 兜底判海外。 CR-B(character_card_manager.js):自定义音色下拉的选中同步按 value 全量比较, 海外列表里 default(pin) 与 Leda(原生) voice_id 同为 "Leda"(刻意不去重)会多项 同时高亮。改为只高亮第一个匹配项,与原生 <select> 重复 value 下 selectedIndex 落第一个的语义对齐。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * revert: 从本 PR 撤掉 aiCompanion i18n key(i18n-sync lockstep 阻断) check_i18n_sync.py 要求 8 个 locale 改动落在完全相同的行号范围。aiCompanion 区域 en/zh-CN 与其余 6 个 locale 存在 2 行预先漂移(非本 PR 引入),导致新增 key 在各文件落到不同行、lockstep 失败。该 key 与海外语音功能无关,先从本 PR 撤出以解阻断;aiCompanion 缺口 + locale 行对齐另开 PR 处理。voice.freeVoice 区域 8 文件对齐,保留。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: 海外免费长列表也隐藏撞名 Gemini 音色(Codex P2) 撞名 pin 隐藏(#2)只处理了置顶项;跨 api-key 桶存在同名克隆时,native 长 列表里的同名 Gemini 条目(如 Leda)仍展示,但 runtime 路由/preview 用 any-storage 撞名判定会拒绝当 native,点选到不了 Gemini。前端只按当前 api 的 voices 去重、跨桶撞名漏网,故在 /voices 后端对 free_intl 长列表按同一谓词 收口,与 pin 撞名隐藏对偶。加回归测试。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * fix: TTS language_code 补葡萄牙语映射(Codex P2) TTS_LANGUAGE_CODE_MAP 缺 'pt',葡语(normalize→'pt')会兜底成 cmn-CN,海外 lanlan.app 的 streaming TTS 与 realtime session 都会给葡语用户下发普通话 language_code。补 'pt' -> 'pt-BR'(与 Gemini/Google 系 locale code 一致)。 加测试。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Hongzhi Wen <cartabio.coder1@gmail.com> Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent acae8f3 commit baa6088

25 files changed

Lines changed: 610 additions & 216 deletions

config/api_providers.json

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -367,6 +367,7 @@
367367
},
368368
"_comment_free_voices": "Shape: {voiceKey: voice_id}. Keys are i18n identifiers (e.g., playfulGirl, cuteGirl) that the frontend must localize for display. Do not treat keys as display names.",
369369
"free_voices": {
370+
"yui_cn": "voice-tone-RcH2svtsrw",
370371
"playfulGirl": "voice-tone-PGLiTXeJCS",
371372
"cuteGirl": "voice-tone-PGLiyZt65w",
372373
"cuteMaiden": "voice-tone-PGLjbHIMbI",
@@ -376,8 +377,7 @@
376377
"gentleMaiden": "voice-tone-PGLlrd5SNM",
377378
"sweetLady": "voice-tone-PGLmTEeUOu",
378379
"frailMaiden": "voice-tone-PGLmzXEX44",
379-
"childishMaiden": "voice-tone-PGLnVNQn7A",
380-
"yui_cn": "voice-tone-RcH2svtsrw"
380+
"childishMaiden": "voice-tone-PGLnVNQn7A"
381381
},
382382
"_comment_native_tts_voice_providers": "Shape: {providerKey: {catalog_prefix, default_voice, default_male_voice, catalog_value_is_display_name, voices, aliases, inherits}}. voices maps upstream voice_id to display label. Keep this list to voices available on realtime/free TTS paths; some HTTP-only or permission-gated official voices are intentionally omitted.",
383383
"native_tts_voice_providers": {
@@ -462,6 +462,19 @@
462462
"中文女": "Leda"
463463
}
464464
},
465+
"free_intl": {
466+
"inherits": "gemini",
467+
"catalog_prefix": "Gemini",
468+
"default_voice": "yui",
469+
"default_male_voice": "Puck",
470+
"voices": {
471+
"yui": "Female"
472+
},
473+
"aliases": {
474+
"default": "Leda",
475+
"默认": "Leda"
476+
}
477+
},
465478
"grok": {
466479
"catalog_prefix": "Grok",
467480
"default_voice": "eve",

main_logic/core.py

Lines changed: 35 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -504,8 +504,6 @@ def _render_pending_extra_replies_by_origin(
504504
from utils.native_voice_registry import (
505505
is_free_preset_voice_id,
506506
resolve_native_voice_for_routing,
507-
should_block_free_native_voice,
508-
should_block_free_voice_for_route,
509507
)
510508
from utils.api_config_loader import (
511509
get_livestream_config,
@@ -761,7 +759,7 @@ def __init__(self, sync_message_queue, lanlan_name, lanlan_prompt):
761759
self.core_api_type = realtime_config.get('api_type', '') or self._config_manager.get_core_config().get('CORE_API_TYPE', '')
762760
self.memory_server_port = MEMORY_SERVER_PORT
763761
self.audio_api_key = self._config_manager.get_core_config()['AUDIO_API_KEY'] # 用于CosyVoice自定义音色
764-
self._apply_voice_id_for_route(realtime_config.get('base_url', ''))
762+
self._apply_voice_id_for_route()
765763
# 注意:use_tts 会在 start_session 中根据 input_mode 重新设置
766764
self.use_tts = False
767765
self.generation_config = {} # Qwen暂时不用
@@ -3377,13 +3375,22 @@ async def _init_renew_status(self):
33773375
# auto-start 不被误清),但 end_session 语义就是整轮收尾,必须强制清场。
33783376
await self.state.reset(force=True)
33793377

3378+
def _realtime_base_url(self) -> str:
3379+
"""读取 realtime 线路 base_url,供 native voice 路由的 host 重映射
3380+
(海外免费 free→free_intl)使用。读不到时返回空串,按非 lanlan.app 处理。"""
3381+
try:
3382+
return str((self._config_manager.get_model_api_config('realtime') or {}).get('base_url') or '')
3383+
except Exception:
3384+
return ''
3385+
33803386
def _has_custom_tts(self) -> bool:
33813387
"""判断当前会话是否使用自定义 TTS(克隆音色或自定义 TTS URL)。"""
33823388
core_config = self._config_manager.get_core_config()
33833389
_, uses_provider_native_voice = resolve_native_voice_for_routing(
33843390
self.core_api_type,
33853391
self.voice_id,
33863392
self._config_manager.voice_id_exists_in_any_storage,
3393+
realtime_base_url=self._realtime_base_url(),
33873394
)
33883395
if uses_provider_native_voice:
33893396
return False
@@ -3722,17 +3729,12 @@ def _resolve_session_use_tts(
37223729
logger.info(f"{log_prefix}🎙️ livestream 模式:使用服务端原生语音,跳过外部 TTS")
37233730
return False
37243731
base_url = realtime_config.get('base_url', '')
3725-
if should_block_free_native_voice(
3726-
self.core_api_type, self.voice_id, base_url,
3732+
_, uses_provider_native_voice = resolve_native_voice_for_routing(
3733+
self.core_api_type,
3734+
self.voice_id,
37273735
self._config_manager.voice_id_exists_in_any_storage,
3728-
):
3729-
uses_provider_native_voice = False
3730-
else:
3731-
_, uses_provider_native_voice = resolve_native_voice_for_routing(
3732-
self.core_api_type,
3733-
self.voice_id,
3734-
self._config_manager.voice_id_exists_in_any_storage,
3735-
)
3736+
realtime_base_url=base_url,
3737+
)
37363738
if uses_provider_native_voice:
37373739
logger.info(f"{log_prefix}🔊 {self.core_api_type} 原生音色 '{self.voice_id}' 将直接传入 RealtimeClient")
37383740
return False
@@ -3760,24 +3762,23 @@ def _get_voice_id(self) -> str:
37603762
# 比较 / route gating / is_free_preset_voice_id 之类的 callee 失配。
37613763
return (raw or '').strip()
37623764

3763-
def _apply_voice_id_for_route(self, realtime_base_url: str) -> None:
3765+
def _apply_voice_id_for_route(self) -> None:
37643766
"""按当前 route 把角色卡里的 voice_id 解析进 self.voice_id /
37653767
self._is_free_preset_voice。
37663768
37673769
__init__ / start_session / _background_prepare_pending_session 三处
3768-
共用:读取 _get_voice_id() → 海外 free 路由屏蔽 → 校正 free preset
3769-
与 core_api_type 的匹配关系。集中在这里避免规则漂移。
3770+
共用:读取 _get_voice_id() → 校正 free preset 与 core_api_type 的匹配
3771+
关系。集中在这里避免规则漂移。
3772+
3773+
历史上这里还按"海外 lanlan.app 会硬覆盖成 Leda"屏蔽 voice 下发;
3774+
现在海外免费统一走 www.lanlan.app 透传 voice(Gemini 全量 + yui,由
3775+
free_intl provider 认领),不再屏蔽——stale 的阶跃/free 预设音色在海外
3776+
路由下不会命中 free_intl catalog,自然 fall through,不需要预清。
3777+
3778+
空 voice_id 保持空:海外免费下"空 → 默认音色"的映射交给服务端
3779+
(www.lanlan.app)处理,客户端不再注入兜底音色。
37703780
"""
37713781
raw_voice_id = self._get_voice_id()
3772-
if should_block_free_voice_for_route(
3773-
self.core_api_type,
3774-
raw_voice_id,
3775-
realtime_base_url,
3776-
self._config_manager.voice_id_exists_in_any_storage,
3777-
):
3778-
self.voice_id = ''
3779-
self._is_free_preset_voice = False
3780-
return
37813782
self.voice_id = raw_voice_id
37823783
self._is_free_preset_voice = is_free_preset_voice_id(raw_voice_id)
37833784
# free preset 选了但当前非 free 模式 → 不下发,避免把 preset id 透给别的 provider。
@@ -3799,20 +3800,16 @@ def _resolve_realtime_voice(self, realtime_config: dict):
37993800
(绕过 free_voices preset gate,base_url 已被派生不含 lanlan.tech)
38003801
3. 否则保留原逻辑:仅在角色 voice 是 free preset、core_api_type='free'
38013802
且 base_url 仍指向 lanlan.tech 域时下发,避免把 preset id 透给非
3802-
lanlan 服务(lanlan.app 的屏蔽由 should_block_free_voice_for_route 兜底)
3803+
lanlan 服务。海外免费(free + *.lanlan.app)的 yui / Gemini 音色由
3804+
resolve_native_voice_for_routing 经 free_intl 重映射在第 1 步直接命中。
38033805
"""
38043806
base_url = realtime_config.get('base_url', '')
3805-
if should_block_free_native_voice(
3806-
self.core_api_type, self.voice_id, base_url,
3807+
voice_name, uses_provider_native_voice = resolve_native_voice_for_routing(
3808+
self.core_api_type,
3809+
self.voice_id,
38073810
self._config_manager.voice_id_exists_in_any_storage,
3808-
):
3809-
voice_name, uses_provider_native_voice = self.voice_id, False
3810-
else:
3811-
voice_name, uses_provider_native_voice = resolve_native_voice_for_routing(
3812-
self.core_api_type,
3813-
self.voice_id,
3814-
self._config_manager.voice_id_exists_in_any_storage,
3815-
)
3811+
realtime_base_url=base_url,
3812+
)
38163813
if uses_provider_native_voice:
38173814
return voice_name
38183815
if self._is_livestream_active():
@@ -4177,7 +4174,7 @@ async def start_session(self, websocket: WebSocket, new=False, input_mode='audio
41774174
# 重新读取角色配置以获取最新的voice_id(支持角色切换后的音色热更新)
41784175
_, _, _, self.lanlan_basic_config, _, _, _, _, _ = await self._config_manager.aget_character_data()
41794176
old_voice_id = self.voice_id
4180-
self._apply_voice_id_for_route(realtime_config.get('base_url', ''))
4177+
self._apply_voice_id_for_route()
41814178

41824179
# 如果角色没有设置 voice_id,尝试使用自定义API配置的 TTS_VOICE_ID 作为回退
41834180
if not self.voice_id:
@@ -4866,7 +4863,7 @@ async def _background_prepare_pending_session(self):
48664863
# 重新读取角色配置以获取最新的voice_id(支持角色切换后的音色热更新)
48674864
_, _, _, self.lanlan_basic_config, _, _, _, _, _ = await self._config_manager.aget_character_data()
48684865
old_voice_id = self.voice_id
4869-
self._apply_voice_id_for_route(realtime_config.get('base_url', ''))
4866+
self._apply_voice_id_for_route()
48704867

48714868
# 如果角色没有设置 voice_id,尝试使用自定义API配置的 TTS_VOICE_ID 作为回退
48724869
if not self.voice_id:

main_logic/omni_realtime_client.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -983,6 +983,12 @@ async def connect(self, instructions: str, native_audio=True) -> None:
983983
"type": "server_vad"
984984
},
985985
}
986+
# 海外免费(lanlan.app,Gemini 代理)建 session 时一次性指定
987+
# language_code,与 TTS server 路对偶;lanlan.tech(StepFun)不发,
988+
# 沿用其自动识别 / voice_label 语义。
989+
if 'lanlan.app' in (self.base_url or ''):
990+
from utils.language_utils import get_tts_language_code
991+
free_session["language_code"] = get_tts_language_code()
986992
free_tools: List[Dict[str, Any]] = []
987993
if self.has_tools():
988994
free_tools.extend(self._tools_for_step())

main_logic/tts_client.py

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -233,30 +233,14 @@ def _ws_is_open(ws_conn) -> bool:
233233
return not getattr(ws_conn, "closed", True)
234234

235235

236-
_TTS_LANGUAGE_CODE_MAP = {
237-
'zh': 'cmn-CN',
238-
'zh-CN': 'cmn-CN',
239-
'zh-TW': 'cmn-tw',
240-
'en': 'en-US',
241-
'ja': 'ja-JP',
242-
'ko': 'ko-KR',
243-
'es': 'es-ES',
244-
'fr': 'fr-FR',
245-
'de': 'de-DE',
246-
'it': 'it-IT',
247-
'ru': 'ru-RU',
248-
'tr': 'tr-TR'
249-
}
250-
251-
252236
def _get_tts_language_code() -> str:
253-
"""获取 lanlan.app TTS 服务器所需的 language_code。"""
254-
try:
255-
from utils.language_utils import get_global_language_full, normalize_language_code
256-
lang = normalize_language_code(get_global_language_full(), format='full')
257-
except Exception:
258-
lang = 'zh-CN'
259-
return _TTS_LANGUAGE_CODE_MAP.get(lang, 'cmn-CN')
237+
"""获取 lanlan.app TTS 服务器所需的 language_code。
238+
239+
实现收敛到 utils.language_utils.get_tts_language_code —— core/realtime 与
240+
TTS server 两条路共用同一张 BCP-47 映射表,避免漂移。
241+
"""
242+
from utils.language_utils import get_tts_language_code
243+
return get_tts_language_code()
260244

261245

262246
def _build_step_tts_create_data(sid_: str, voice_id: str, lang_hint, is_lanlan_app: bool) -> dict:
@@ -268,7 +252,8 @@ def _build_step_tts_create_data(sid_: str, voice_id: str, lang_hint, is_lanlan_a
268252
"sample_rate": 24000,
269253
}
270254
if is_lanlan_app:
271-
data["voice_id"] = "Leda"
255+
# 发真实 voice_id(data 里已带传入值),由 www.lanlan.app 服务端透传给
256+
# Gemini 并做映射;不再客户端硬覆盖成 Leda。
272257
data["language_code"] = "ja-JP" if lang_hint == "ja" else _get_tts_language_code()
273258
else:
274259
# lanlan.tech (free) 和自建 StepFun 协议对称,都用 voice_label。
@@ -1384,6 +1369,18 @@ async def receive_messages():
13841369
worker_kwargs={'free_mode': True},
13851370
),
13861371
)
1372+
# free_intl(海外免费 *.lanlan.app):上游 Gemini 代理走 www.lanlan.app/tts,
1373+
# 协议同 free(StepFun-shape streaming,proxy 把 voice_id 透传给 Gemini),
1374+
# 因此复用 free 的 worker。与 free 对偶,仅 provider key 不同(registry 按
1375+
# host 把 free→free_intl 重映射,让 yui/Gemini 音色短路到这里而非外部 TTS)。
1376+
register_tts_worker_resolver(
1377+
'free_intl',
1378+
make_native_tts_resolver(
1379+
step_realtime_tts_worker,
1380+
'tts_default_api_key',
1381+
worker_kwargs={'free_mode': True},
1382+
),
1383+
)
13871384

13881385

13891386
# xAI 文档:'Individual deltas are capped at 15,000 characters'。

main_routers/characters_router.py

Lines changed: 64 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3809,6 +3809,39 @@ async def get_microphone():
38093809
return {"microphone_id": None}
38103810

38113811

3812+
def _build_free_intl_voice_pins(native_catalog: dict, voice_id_exists=None) -> list[dict]:
3813+
"""海外免费(free_intl)列表顶部的两个置顶音色。
3814+
3815+
- yui:初始/默认角色音色,下发字面量 "yui"(服务端映射到 yui 专属声音)。
3816+
- default:与 Leda 同义,下发 "Leda"。仍以普通条目保留在 Gemini 长列表里
3817+
(不去重),这里只是把它再置顶一份并换成 "默认" 文案。
3818+
3819+
展示名交给前端按 i18n_key 本地化;这里只给 voice_id / i18n_key / 兜底 prefix。
3820+
3821+
voice_id_exists:若某 pin 的 voice_id 与用户已注册/克隆音色撞名(如本地 TTS
3822+
用户自建了一个 ID 叫 "yui"/"Leda" 的音色),runtime 路由会按撞名优先走克隆
3823+
路径、不再当 native(见 NativeVoiceProvider.resolve_for_routing 的 collision
3824+
分支),此时置顶 pin 点了也到不了 Gemini,故直接隐藏,避免误导。
3825+
"""
3826+
def _pin(voice_id: str, i18n_key: str, fallback: str) -> dict | None:
3827+
if callable(voice_id_exists) and voice_id_exists(voice_id):
3828+
return None
3829+
meta = native_catalog.get(voice_id) or {}
3830+
return {
3831+
"voice_id": voice_id,
3832+
"i18n_key": i18n_key,
3833+
"prefix": meta.get("prefix") or fallback,
3834+
"provider": "free_intl",
3835+
"builtin": True,
3836+
}
3837+
3838+
pins = [
3839+
_pin("yui", "voice.freeVoice.yui", "Yui"),
3840+
_pin("Leda", "voice.freeVoice.default", "Default"),
3841+
]
3842+
return [pin for pin in pins if pin is not None]
3843+
3844+
38123845
@router.get('/voices')
38133846
async def get_voices():
38143847
"""获取当前API key对应的所有已注册音色"""
@@ -3818,7 +3851,33 @@ async def get_voices():
38183851
core_config = await _config_manager.aget_core_config()
38193852
active_native_provider = get_active_realtime_native_provider_for_ui(_config_manager)
38203853
if active_native_provider:
3821-
result["native_voices"] = get_native_voice_catalog_for_ui(active_native_provider)
3854+
native_catalog = get_native_voice_catalog_for_ui(active_native_provider) or {}
3855+
if active_native_provider == 'free_intl':
3856+
# 海外免费(lanlan.app/Gemini):yui + default(=Leda) 两个置顶 pin,
3857+
# 其后是 Gemini 全量目录。yui 从长列表里挪到 pin(不重复展示);
3858+
# Leda 不去重,仍作为普通条目留在长列表里(= default pin 的目标)。
3859+
# pin 的展示名由前端按 i18n_key 本地化。
3860+
voice_exists = getattr(_config_manager, "voice_id_exists_in_any_storage", None)
3861+
result["pinned_voices"] = _build_free_intl_voice_pins(
3862+
native_catalog,
3863+
voice_id_exists=voice_exists,
3864+
)
3865+
# 撞名(跨 api-key 桶存在同名克隆/自定义音色)的条目也从长列表里去掉:
3866+
# runtime 路由/preview 用 any-storage 撞名判定会拒绝当 native,展示了
3867+
# 点选也到不了 Gemini(与 pin 的撞名隐藏对偶)。前端只按当前 api 的
3868+
# voices 去重,跨桶撞名漏网,故在后端按同一谓词收口。
3869+
def _free_intl_keep(voice_id: str) -> bool:
3870+
if voice_id == 'yui':
3871+
return False
3872+
if callable(voice_exists) and voice_exists(voice_id):
3873+
return False
3874+
return True
3875+
native_catalog = {
3876+
voice_id: meta
3877+
for voice_id, meta in native_catalog.items()
3878+
if _free_intl_keep(voice_id)
3879+
}
3880+
result["native_voices"] = native_catalog
38223881

38233882
# 免费预设音色只在 core=free 运行时可用(与 assist 无关);core_url 仍须指向
38243883
# lanlan.tech 免费端点,海外 lanlan.app 路由由 should_block_free_voice_for_route 兜底。
@@ -3904,11 +3963,13 @@ async def get_voice_preview(
39043963
if native_preview_provider:
39053964
native_voice_id, _ = normalize_native_voice(native_preview_provider, voice_id)
39063965
try:
3907-
if native_preview_provider in ('step', 'free'):
3966+
if native_preview_provider in ('step', 'free', 'free_intl'):
39083967
# 只读 tts_default.api_key —— 跟 step_realtime_tts_worker 走的 key 对偶;
39093968
# 不能回退到 audio_api_key(顶上从 tts_custom / AUDIO_API_KEY 取的,都是
39103969
# GPT-SoVITS / CosyVoice 这种别家 provider 的 bearer,把它透给
39113970
# api.stepfun.com 一律 401,错误现象比明确缺 key 难排查。
3971+
# free_intl(海外免费 Gemini 代理)预览同 free,走 www.lanlan.app/tts
3972+
# 流式合成(StepFun-shape,proxy 把 voice_id 透传给 Gemini)。
39123973
try:
39133974
native_tts_config = _config_manager.get_model_api_config('tts_default')
39143975
native_audio_api_key = native_tts_config.get('api_key', '') or ''
@@ -3925,7 +3986,7 @@ async def get_voice_preview(
39253986
preview_line=text,
39263987
preview_language=preview_language,
39273988
audio_api_key=native_audio_api_key,
3928-
free_mode=(native_preview_provider == 'free'),
3989+
free_mode=(native_preview_provider in ('free', 'free_intl')),
39293990
)
39303991
elif native_preview_provider == 'gemini':
39313992
core_config = await _config_manager.aget_core_config()

0 commit comments

Comments
 (0)