fix(voice): 自动平移废弃的免费 YUI 预设音色,避免存量用户掉档到 qingchunshaonv#1665
Conversation
#1639 把 free_voices["yui_cn"] 从 voice-tone-R6NtLH3Hk0 换成 voice-tone-RcH2svtsrw, 但只改了 config 模板和解析源,没迁移已写进存量 characters.json 的旧 tone ID。旧值已 不在 free_voices 白名单 → cleanup_invalid_voice_ids 判 invalid 清空 → 空 voice 落到 free/step 的 default_voice(qingchunshaonv),导致「一直吃默认 YUI、从没手动选过音色」 的免费用户在音色 ID 更替后无声掉档到通用女声。 在 cleanup 判 invalid 前先把废弃 YUI tone 平移到现役 yui_cn: - 新增 _DEPRECATED_FREE_YUI_VOICE_IDS 集合 + is_deprecated_free_yui_voice_id / remap_deprecated_free_yui_voice_id(对偶既有 is_legacy_cosyvoice_id) - 现役值解析不出(或仍落废弃集合)时不乱换,交既有清空兜底,绝不废弃换废弃 - 迁移命中触发存盘;覆盖 app 初始化与每次 start_session 两条 cleanup 路径 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
|
Need an answer fast? Review this PR in Change Stack to ask focused questions about the PR or a changed range. No actionable comments were generated in the recent review. 🎉 ℹ️ Recent review info⚙️ Run configurationConfiguration used: Repository UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
Walkthrough新增废弃免费 YUI voice_id 常量与 remap/is_deprecated 方法;在 cleanup_invalid_voice_ids 中优先对命中废弃 ID 做 remap 并计入 migrated_count,随后在 cleaned_count 或 migrated_count 任一非零时保存 characters.json;新增单元测试覆盖多种迁移与清理边界场景喵。 Changes废弃YUI音色迁移完整实现
Sequence DiagramsequenceDiagram
participant Cleanup as cleanup_invalid_voice_ids()
participant Detect as ConfigManager.is_deprecated_free_yui_voice_id()
participant Remap as ConfigManager.remap_deprecated_free_yui_voice_id()
participant FreePresets as utils.api_config_loader.get_free_voices()
participant Character as character._reserved.voice_id
participant Save as save_characters()
Cleanup->>Character: 读取角色的 voice_id
Cleanup->>Detect: 判定是否为废弃免费 YUI ID
Detect-->>Cleanup: True / False
alt 命中废弃集合
Cleanup->>Remap: 调用 remap_deprecated_free_yui_voice_id(voice_id)
Remap->>FreePresets: 读取 free_voices(判断 yui_cn)
Remap-->>Cleanup: 返回替换值(解析到 yui_cn 或返回原值)
Cleanup->>Character: 若替换成功则更新 voice_id;migrated_count += 1
else 非废弃或替换失败
Cleanup->>Cleanup: 进行原有校验;若无效则清空 voice_id;cleaned_count += 1
end
Cleanup->>Cleanup: 检查 cleaned_count 或 migrated_count
alt 任一大于 0
Cleanup->>Save: 调用 save_characters()
end
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~20 minutes Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@utils/config_manager.py`:
- Around line 2905-2920: The code currently compares the raw voice_id to
remap_deprecated_free_yui_voice_id() and may treat values that only differ by
whitespace as "remapped" and skip validation; fix by normalizing the original
before remap and only treat as migrated when the normalized original is actually
a deprecated value: retrieve voice_id via get_reserved, compute
normalized_original = voice_id.strip() (or other project normalization), then
call remap_deprecated_free_yui_voice_id(normalized_original); only if remapped
is non-empty and remapped != normalized_original call set_reserved(config,
'voice_id', remapped), increment migrated_count, log, and continue — otherwise
assign the normalized value back (or fall through to validate_voice_id) so
validate_voice_id can clean/clear truly invalid IDs.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 20d0cf1f-dae2-48c9-ac10-ea38f039b914
📒 Files selected for processing (2)
tests/unit/test_deprecated_yui_voice_migration.pyutils/config_manager.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 94fa15a454
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| # 已废弃的免费 YUI 预设音色:先平移到现役 yui_cn,再 continue 跳过后续 | ||
| # invalid 判定(新值在 free_voices 白名单内本就合法),保住默认 YUI 音色 | ||
| remapped = self.remap_deprecated_free_yui_voice_id(voice_id) | ||
| if remapped and remapped != voice_id: |
There was a problem hiding this comment.
Avoid treating every stripped voice ID as a YUI migration
When a stored non-YUI voice_id has leading/trailing whitespace, remap_deprecated_free_yui_voice_id() returns the stripped value, so this condition treats it as a successful deprecated-YUI migration and continues before validate_voice_id() runs. For example, a stale value like some-stale-clone is saved as some-stale-clone and reported as migrated instead of being cleared during this cleanup pass, leaving an invalid voice active until a later cleanup.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已采纳,33a14daa6 修复(与上面 CodeRabbit 同一处)。非废弃值现在原样返回原 voice_id、不再 strip,带空白的非废弃 voice_id 不会被当成 YUI 迁移而跳过清理。
| current = _get_default_yui_free_voice_id() | ||
| if current and current not in _DEPRECATED_FREE_YUI_VOICE_IDS: | ||
| return current |
There was a problem hiding this comment.
Require yui_cn before remapping retired YUI IDs
In configurations where free_voices still contains other presets but no yui_cn entry, _get_default_yui_free_voice_id() falls back to cuteGirl or the first available preset. That makes this migration rewrite the retired YUI tone to an unrelated voice instead of leaving it for the existing validation/clear fallback, which is the documented behavior when the current YUI value cannot be resolved.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Good catch,已采纳,33a14daa6 修复。remap 不再复用 _get_default_yui_free_voice_id(它带 cuteGirl/首个 preset 的宽松 fallback,那是给 ensure_default_yui_voice_for_free_api 的容错语义),改为直接读 free_voices["yui_cn"]——废弃值本就是 yui_cn 的旧版本,精确平移回它。yui_cn 缺失/为空/仍落在废弃集合时不迁移、交既有清空兜底,绝不借别的 preset 当替身把 YUI 串成别的音色。已加回归 test_remap_requires_yui_cn_not_other_preset。
CodeRabbit/Codex: remap 对非废弃值做了 strip 归一化,带前后空白的无效 voice_id 会 因 remapped != voice_id 被误当「已迁移」continue,漏掉本轮 invalid 清理 → 改为非废弃 值原样返回原值(strip 收口在 _get_voice_id 源头,不是 remap 的职责)。 Codex: 复用 _get_default_yui_free_voice_id 会在 free_voices 缺 yui_cn 时 fallback 到 cuteGirl/首个 preset,把废弃 YUI 串成别的音色 → 改为直接读 free_voices["yui_cn"], 缺失/为空/仍落废弃集合时不迁移,交既有清空兜底。 新增「带空白废弃仍迁移」「带空白非废弃 invalid 仍清空」「缺 yui_cn 不借替身」三条回归。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
🧹 Nitpick comments (1)
tests/unit/test_deprecated_yui_voice_migration.py (1)
51-61: 💤 Low value可选建议:补充存盘断言以保持测试一致性喵~
Line 60 验证了 voice_id 迁移成功,但没有像 Line 48 那样断言
mgr._saved.get("data") is character_data,检查迁移是否触发了存盘喵。虽然实现逻辑不太可能因空白处理破坏存盘触发,但保持断言一致性能让测试更完整呢喵!✨ 可选补充
assert cleaned == 0 assert get_reserved(character_data["猫娘"]["YUI"], "voice_id", default="") == NEW_YUI_VOICE_ID + assert mgr._saved.get("data") is character_data🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@tests/unit/test_deprecated_yui_voice_migration.py` around lines 51 - 61, The test function test_cleanup_migrates_whitespace_padded_deprecated_voice verifies that voice_id migration occurred but is missing an assertion to confirm that the save operation was triggered by the migration. Add an assertion after the existing assertions (similar to what is done in the test at line 48) to verify that mgr._saved.get("data") returns the character_data object, ensuring that the cleanup operation properly triggered the save mechanism when migrating the whitespace-padded deprecated voice ID.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Nitpick comments:
In `@tests/unit/test_deprecated_yui_voice_migration.py`:
- Around line 51-61: The test function
test_cleanup_migrates_whitespace_padded_deprecated_voice verifies that voice_id
migration occurred but is missing an assertion to confirm that the save
operation was triggered by the migration. Add an assertion after the existing
assertions (similar to what is done in the test at line 48) to verify that
mgr._saved.get("data") returns the character_data object, ensuring that the
cleanup operation properly triggered the save mechanism when migrating the
whitespace-padded deprecated voice ID.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: dc6aaa91-9a2a-433c-8017-8cd07f1e88cf
📒 Files selected for processing (2)
tests/unit/test_deprecated_yui_voice_migration.pyutils/config_manager.py
🚧 Files skipped from review as they are similar to previous changes (1)
- utils/config_manager.py
与 test_cleanup_migrates_deprecated_yui_voice 对偶,验证带前后空白的废弃值迁移 路径同样触发 save_characters,而非只改了内存对象。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: a80476656d
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if not self.is_deprecated_free_yui_voice_id(voice_id): | ||
| return voice_id | ||
| from utils.api_config_loader import get_free_voices | ||
| current = str((get_free_voices() or {}).get("yui_cn") or "").strip() |
There was a problem hiding this comment.
Gate deprecated YUI remaps by route
When a free user is on the overseas lanlan.app route and still has this deprecated StepFun YUI tone in characters.json, remapping it unconditionally to free_voices["yui_cn"] preserves a domestic free preset where the overseas path expects the literal yui/Gemini catalog instead. I checked main_logic/core.py: _resolve_session_use_tts() only treats free presets as native on lanlan.tech, and otherwise a nonempty self.voice_id falls into external TTS, while native_voice_registry remaps free+lanlan.app to free_intl where yui_cn is not a native voice. Before this change the stale ID was cleared by validation, so this can regress those users from the intended overseas native/default voice path to an external TTS path.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
Confirmed,好 catch,af8e26e14 修复。你说的对:海外 lanlan.app→free_intl 不认 voice-tone-*,无条件 remap 成国内 yui_cn 会让非空 voice_id 在 _resolve_session_use_tts(free preset 只在 lanlan.tech 当 native)落进 external TTS,对从国内切到海外的存量用户是 regression。
已把 remap 改成按线路分流,对偶 ensure_default_yui_voice_for_free_api 的 "yui"/yui_cn 二分:仅 core=free 才迁移(非 free 路由原样返回交清空兜底);海外 free(is_free_lanlan_app_route + _check_non_mainland 兜底)→ 品牌 sentinel "yui"(free_intl native,走海外原生语音);国内 free(lanlan.tech)→ 现役 free_voices["yui_cn"]。新增海外绑 yui / 地理兜底 / 非 free 不迁移三条回归。
There was a problem hiding this comment.
更正一下我上一条回复的方向:78a618b7a 起,海外 free 不再 remap 成 "yui"。经核对 PR #1643 的既有契约——海外 free(lanlan.app→free_intl)下 stale/未知 voice id 应交服务端默认音色 fallback,客户端不得注入 yui/native alias(free_intl 继承 Gemini-native provider,StepFun magic id 及其 alias 都不该漏进该 catalog)。所以现在只有国内 free(lanlan.tech)迁移到 yui_cn;海外 free 原样返回交既有 validate 清空 → 落服务端默认,与你最初指出的「按路由 gate、别保留国内 preset」一致。
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>
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@tests/unit/test_deprecated_yui_voice_migration.py`:
- Around line 6-8: The tests in
tests/unit/test_deprecated_yui_voice_migration.py currently assert that overseas
free IDs are remapped to "yui" which breaks the contract for free_intl; update
the affected test cases (those asserting voice_id == "yui" for
overseas/free_intl scenarios, and any tests around
ensure_default_yui_voice_for_free_api, cleanup, or StepFun ID handling) to
instead assert that no "yui" remapping occurs — i.e., that the original voice_id
is preserved (or equals the non-remap result produced by cleanup) for
free_intl/overseas routes; keep assertions explicit: check voice_id != "yui"
and/or voice_id == original_id (or the cleanup-returned id) rather than forcing
"yui".
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 952d6db7-6ae9-4aee-9417-9d9ed89ac45b
📒 Files selected for processing (2)
tests/unit/test_deprecated_yui_voice_migration.pyutils/config_manager.py
🚧 Files skipped from review as they are similar to previous changes (1)
- utils/config_manager.py
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: af8e26e142
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| if overseas: | ||
| return "yui" |
There was a problem hiding this comment.
Remap persisted yui when returning domestic
For users whose deprecated YUI tone is first cleaned on the overseas lanlan.app route, this persists the route-specific sentinel "yui" and skips validation. If the same character later runs on the domestic lanlan.tech free route, validate_voice_id() does not accept "yui" there (the effective native provider is free, and free_voices contains the StepFun yui_cn tone instead), so the next cleanup clears the voice and the user falls back to qingchunshaonv; the original deprecated ID has already been overwritten, so it can no longer be mapped to yui_cn. Consider also migrating saved "yui" back to the current yui_cn on the domestic free route, or avoid persisting a route-specific sentinel as the only migration state.
Useful? React with 👍 / 👎.
There was a problem hiding this comment.
已采纳你的第二个建议(不持久化 route-specific sentinel),78a618b7a 修复。海外 free 不再 remap 成 "yui",而是原样返回交既有 validate 清空成空串(而非 "yui")。空 voice 切回国内时由 update_core_config → ensure_default_yui_voice_for_free_api 重绑现役 yui_cn(与默认 YUI 首次绑定同源),不会停在 qingchunshaonv。关键区别:空值能触发 ensure_default 重绑,而 af8e26e 存的非空 "yui" 会让 ensure_default 因 voice 非空跳过、再被 cleanup 清空,正是你指出的掉档路径。
af8e26e 让海外 free 把废弃 StepFun YUI tone remap 成 "yui" sentinel,违反 PR #1643 确立的设计原则:海外 free(lanlan.app→free_intl)下未知/stale voice id 应交服务端默认 音色 fallback,客户端不得注入 yui 或 native alias(free_intl 继承 Gemini-native provider,不能把 StepFun magic id/alias 漏进该 catalog)。 remap 改为只有国内 free(lanlan.tech)才迁移到现役 yui_cn;海外 free 与非 free 路由均 原样返回,交既有 validate 判 invalid 清空 → 落服务端默认。两种线路运行时都得到正确 YUI 音色,且 cross-region 往返更稳(切回国内由 ensure_default 兜底重绑)。 测试相应改为海外不 remap(原样返回 / cleanup 清空),删掉 "yui" sentinel 断言。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
背景
#1639 把
free_voices[\"yui_cn\"]从voice-tone-R6NtLH3Hk0换成voice-tone-RcH2svtsrw,但只改了 config 模板和解析源,没有迁移已写进存量用户characters.json的旧 tone ID。问题(国内免费版静默掉档)
旧值
voice-tone-R6NtLH3Hk0现在不在free_voices白名单里,于是每次start_session(main_logic/core.py)和 app 初始化(app/main_server.py)跑的cleanup_invalid_voice_ids会把它判为 invalid 清空:validate_voice_id三项落空(非云端克隆 / 非原生音色 / 不在 free_voices)→ 清空成''ensure_default_yui_voice_for_free_api只在「保存 API 配置」「清除音色」两个用户主动动作里跑)freeprovider(inherits: step)的default_voice=qingchunshaonv结果:「一直吃默认 YUI、从没手动选过音色」的国内免费用户,下次启动就无声掉档到通用女声。海外免费版默认存的是字面量
yui(free_intl catalog 内,cleanup 会放行),不受影响。改动
在
cleanup_invalid_voice_ids判 invalid 之前先把废弃 YUI tone 平移到现役yui_cn:_DEPRECATED_FREE_YUI_VOICE_IDS集合 +is_deprecated_free_yui_voice_id/remap_deprecated_free_yui_voice_id(对偶既有is_legacy_cosyvoice_id)start_session两条 cleanup 路径,存量用户无需任何手动操作测试
tests/unit/test_deprecated_yui_voice_migration.py:平移命中、现役值不动且不存盘、无关 invalid 仍清空(回归)、现役不可解析时不乱换、谓词边界。本地test_deprecated_yui_voice_migration+test_yui_free_api_default_voice+ gemini/native/stepfun voice 套件共 64 passed。🤖 Generated with Claude Code
Summary by CodeRabbit
测试
改进