Skip to content

Commit af8e26e

Browse files
Hongzhi Wenclaude
andcommitted
fix(voice): 废弃 YUI remap 按线路分流,海外 free 绑 yui sentinel(Codex P2)
Codex: 海外 free(lanlan.app→free_intl)用户若残留国内 StepFun YUI tone,无条件 remap 成 voice-tone yui_cn 仍是国内 preset;海外不认 voice-tone-*,非空 voice_id 会 从海外 native/default(yui) 路径 regress 到 external TTS(_resolve_session_use_tts 仅 在 lanlan.tech 把 free preset 当 native)。改前 stale id 本会被 validation 清空。 remap 改为对偶 ensure_default_yui_voice_for_free_api 的线路二分: - 仅 core=free 才迁移;非 free 路由原样返回交清空兜底 - 海外 free(lanlan.app / _check_non_mainland 兜底)→ 品牌 sentinel "yui" - 国内 free(lanlan.tech)→ 现役 free_voices["yui_cn"] 新增「海外绑 yui」「URL 仍 tech 但地理判海外兜底」「非 free 不迁移」三条用例。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent a804766 commit af8e26e

2 files changed

Lines changed: 74 additions & 13 deletions

File tree

tests/unit/test_deprecated_yui_voice_migration.py

Lines changed: 49 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
1-
"""废弃免费 YUI 预设音色的自动平移(PR: voice-tone-R6NtLH3Hk0 → 现役 yui_cn)。
1+
"""废弃免费 YUI 预设音色的自动平移(PR: voice-tone-R6NtLH3Hk0 → 现役 YUI 音色)。
22
33
回归点:YUI 默认音色 ID 更替后,存量用户 characters.json 里残留的旧 tone ID
44
已不在 free_voices 白名单,cleanup_invalid_voice_ids 会判 invalid 清空 → 空
55
voice 落到 free/step 的 default_voice(qingchunshaonv),导致默认 YUI 用户无声
6-
掉档到通用女声。cleanup 在判 invalid 前先把旧值平移到现役 yui_cn 兜住。
6+
掉档到通用女声。cleanup 在判 invalid 前先把旧值按线路平移:国内 free → 现役
7+
yui_cn;海外 free → 品牌 sentinel "yui"(free_intl native),与
8+
ensure_default_yui_voice_for_free_api 对偶。
79
"""
810
from __future__ import annotations
911

@@ -13,8 +15,13 @@
1315
OLD_YUI_VOICE_ID = "voice-tone-R6NtLH3Hk0"
1416
NEW_YUI_VOICE_ID = "voice-tone-RcH2svtsrw"
1517

18+
_DOMESTIC_FREE = {"CORE_API_TYPE": "free", "CORE_URL": "wss://www.lanlan.tech/core"}
19+
_OVERSEAS_FREE = {"CORE_API_TYPE": "free", "CORE_URL": "wss://www.lanlan.app/core"}
20+
_NON_FREE = {"CORE_API_TYPE": "qwen", "CORE_URL": ""}
1621

17-
def _make_manager(character_data: dict) -> ConfigManager:
22+
23+
def _make_manager(character_data: dict, core_config: dict | None = None,
24+
non_mainland: bool = False) -> ConfigManager:
1825
mgr = object.__new__(ConfigManager)
1926
mgr._saved = {}
2027
mgr.load_characters = lambda: character_data
@@ -23,6 +30,8 @@ def _save(data):
2330
mgr._saved["data"] = data
2431

2532
mgr.save_characters = _save
33+
mgr.get_core_config = lambda: dict(core_config if core_config is not None else _DOMESTIC_FREE)
34+
mgr._check_non_mainland = lambda: non_mainland
2635
return mgr
2736

2837

@@ -62,6 +71,38 @@ def test_cleanup_migrates_whitespace_padded_deprecated_voice(monkeypatch):
6271
assert mgr._saved.get("data") is character_data
6372

6473

74+
def test_cleanup_overseas_free_remaps_to_yui_sentinel(monkeypatch):
75+
"""海外免费(lanlan.app)下废弃 StepFun tone 应绑品牌 sentinel "yui",
76+
而非国内 voice-tone preset——否则非空 voice_id 会落进 external TTS。"""
77+
_patch_free_voices(monkeypatch, {"yui_cn": NEW_YUI_VOICE_ID})
78+
character_data = _yui(OLD_YUI_VOICE_ID)
79+
mgr = _make_manager(character_data, core_config=_OVERSEAS_FREE)
80+
81+
cleaned, legacy = mgr.cleanup_invalid_voice_ids()
82+
83+
assert cleaned == 0
84+
assert get_reserved(character_data["猫娘"]["YUI"], "voice_id", default="") == "yui"
85+
86+
87+
def test_cleanup_overseas_by_geo_remaps_to_yui_sentinel(monkeypatch):
88+
"""URL 仍是 lanlan.tech 但地理判海外时,靠 _check_non_mainland 兜底绑 "yui"。"""
89+
_patch_free_voices(monkeypatch, {"yui_cn": NEW_YUI_VOICE_ID})
90+
character_data = _yui(OLD_YUI_VOICE_ID)
91+
mgr = _make_manager(character_data, core_config=_DOMESTIC_FREE, non_mainland=True)
92+
93+
mgr.cleanup_invalid_voice_ids()
94+
95+
assert get_reserved(character_data["猫娘"]["YUI"], "voice_id", default="") == "yui"
96+
97+
98+
def test_remap_non_free_route_keeps_value(monkeypatch):
99+
"""非 free 路由(如 qwen)下废弃 StepFun preset 用不上,不迁移、交清空兜底。"""
100+
_patch_free_voices(monkeypatch, {"yui_cn": NEW_YUI_VOICE_ID})
101+
mgr = _make_manager({}, core_config=_NON_FREE)
102+
103+
assert mgr.remap_deprecated_free_yui_voice_id(OLD_YUI_VOICE_ID) == OLD_YUI_VOICE_ID
104+
105+
65106
def test_cleanup_keeps_current_yui_voice_untouched(monkeypatch):
66107
_patch_free_voices(monkeypatch, {"yui_cn": NEW_YUI_VOICE_ID})
67108
character_data = _yui(NEW_YUI_VOICE_ID)
@@ -104,18 +145,18 @@ def test_cleanup_clears_whitespace_padded_invalid_voice(monkeypatch):
104145

105146

106147
def test_remap_requires_yui_cn_not_other_preset(monkeypatch):
107-
"""回归(Codex):free_voices 缺 yui_cn、只有别的 preset 时不得借 cuteGirl
108-
等当替身把废弃 YUI 串成别的音色——原样返回,交清空兜底。"""
148+
"""回归(Codex):国内 free 但 free_voices 缺 yui_cn、只有别的 preset 时不得
149+
借 cuteGirl 等当替身把废弃 YUI 串成别的音色——原样返回,交清空兜底。"""
109150
_patch_free_voices(monkeypatch, {"cuteGirl": "voice-tone-PGLiyZt65w"})
110-
mgr = object.__new__(ConfigManager)
151+
mgr = _make_manager({})
111152

112153
assert mgr.remap_deprecated_free_yui_voice_id(OLD_YUI_VOICE_ID) == OLD_YUI_VOICE_ID
113154

114155

115156
def test_remap_keeps_deprecated_when_current_unresolvable(monkeypatch):
116-
"""现役 yui_cn 解析不出(free_voices 为空)时不乱换。"""
157+
"""国内 free 但现役 yui_cn 解析不出(free_voices 为空)时不乱换。"""
117158
_patch_free_voices(monkeypatch, {})
118-
mgr = object.__new__(ConfigManager)
159+
mgr = _make_manager({})
119160

120161
assert mgr.remap_deprecated_free_yui_voice_id(OLD_YUI_VOICE_ID) == OLD_YUI_VOICE_ID
121162
assert mgr.remap_deprecated_free_yui_voice_id(NEW_YUI_VOICE_ID) == NEW_YUI_VOICE_ID

utils/config_manager.py

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2374,18 +2374,38 @@ def is_deprecated_free_yui_voice_id(voice_id) -> bool:
23742374
return bool(voice_id) and str(voice_id).strip() in _DEPRECATED_FREE_YUI_VOICE_IDS
23752375

23762376
def remap_deprecated_free_yui_voice_id(self, voice_id):
2377-
"""废弃的免费 YUI 预设音色 → 现役 yui_cn 值
2377+
"""废弃的免费 YUI 预设音色 → 现役 YUI 音色(按路由判定目标)
23782378
23792379
非废弃值原样返回(不做 strip 归一化),避免调用方把单纯的前后空白差异
23802380
误当成「已迁移」而 continue,漏掉本轮对无效 voice_id 的清理。
23812381
2382-
目标只认 free_voices 的 yui_cn(废弃值本就是它的旧版本);yui_cn 缺失、
2383-
为空、或仍落在废弃集合时同样原样返回,交既有 validate / 清空兜底——绝不
2384-
借用 cuteGirl 等其它 preset 当替身,那会把 YUI 串成别的音色,也不会把废弃
2385-
换成另一个废弃造成死循环。
2382+
废弃值是国内 StepFun YUI tone,只有 core=free 才该迁移,且目标按线路分流,
2383+
与 ensure_default_yui_voice_for_free_api 的 "yui"/yui_cn 二分对偶:
2384+
- 海外免费(lanlan.app → free_intl):voice-tone-* 不是该线路 native,
2385+
换成另一个国内 StepFun tone 反而让非空 voice_id 落进 external TTS;改绑
2386+
品牌 sentinel "yui"(free_intl 的 native/default),走海外原生语音路径。
2387+
- 国内免费(lanlan.tech):换成现役 free_voices["yui_cn"]。
2388+
非 free 路由、或现役 yui_cn 缺失/为空/仍落废弃集合时原样返回,交既有 validate
2389+
清空兜底——绝不借 cuteGirl 等其它 preset 当替身把 YUI 串成别的音色。
23862390
"""
23872391
if not self.is_deprecated_free_yui_voice_id(voice_id):
23882392
return voice_id
2393+
core_cfg = self.get_core_config() or {}
2394+
if (core_cfg.get("CORE_API_TYPE") or core_cfg.get("coreApi")) != "free":
2395+
return voice_id
2396+
2397+
# get_core_config() 已按非大陆把 CORE_URL 改写成 lanlan.app,URL 即可判海外;
2398+
# _check_non_mainland 兜底地理判定。与 ensure_default 同源。
2399+
core_url = str(core_cfg.get("CORE_URL") or "")
2400+
overseas = is_free_lanlan_app_route("free", core_url)
2401+
if not overseas:
2402+
try:
2403+
overseas = bool(self._check_non_mainland())
2404+
except Exception:
2405+
overseas = False
2406+
if overseas:
2407+
return "yui"
2408+
23892409
from utils.api_config_loader import get_free_voices
23902410
current = str((get_free_voices() or {}).get("yui_cn") or "").strip()
23912411
if current and current not in _DEPRECATED_FREE_YUI_VOICE_IDS:

0 commit comments

Comments
 (0)