fix(tts): fall back from missing sprite references and await GPT-SoVITS readiness#248
fix(tts): fall back from missing sprite references and await GPT-SoVITS readiness#248Mizushima-Mihane wants to merge 2 commits into
Conversation
… model switch - reference/preset sprite voices now require the audio file to exist; a stale config path (deleted/moved file) falls back to the character's default reference synthesis (refer_audio_path) instead of sending a missing path to GPT-SoVITS, which fails with HTTP 400 "... not exists". - uploaded sprite voices are named by a hash of their bytes, so replacing a reference voice yields a new path; GPT-SoVITS caches reference features by path, and overwriting the same path kept serving the first reference. - switch_model waits (via _wait_for_http_service_ready, ~10s) for the local server to accept connections before switching weights, avoiding a ConnectionRefusedError on a startup/restart race. Note: "default reference synthesis" = cloning with the character-level refer_audio_path; distinct from the voice_type=="fallback" pre-recorded clip that only plays when no TTS engine is configured.
Reviewer's Guide by Ameath小爱整理了一条清晰的查看路径,我们从关键处慢慢对齐。 审查重点覆盖 文件级变更
Tips and commands
Original review guide in EnglishThe review focused on copy/persistence ordering in |
There was a problem hiding this comment.
小爱把需要留意的地方标出来了,看看这些细节是否符合预期。
小爱已完成全部变更审查,共发布 4 条行级建议(高风险 0,中风险 2)。
合并前关注点(风险 1 · 建议测试 1)
风险
- PR 元数据未列出关联 issue;
CONTRIBUTING.md要求每个 PR 链接既有 issue。
建议测试
DefaultCharacterTtsHandler的回退路径缺少覆盖:当 YAML 中的 spritevoice_path已删除但角色refer_audio_path有效时,应断言合成请求使用角色级默认参考音频;现有test/unit/handlers/test_tts_handlers.py仅验证分派。
Prompt for AI Agents
请逐项处理以下审查问题;修改前先核实上下文,修改后运行相关测试。
1. 无权重切换时不应阻断预录语音 / Do not block preset audio for a no-op model switch
tts/tts_adapter.py:238 (RIGHT)
<issue_to_address>角色的 GPT/SoVITS 路径允许为空,而 DefaultCharacterTtsHandler 会先调用 switch_model,随后才检查 preset。空路径会解析为工作目录,且两个路径都不以 .ckpt/.pth 结尾:旧实现不会发起模型请求,直接播放预录音频;这里却在服务重启或离线时等待并抛出,导致可用的 preset 音频无法播放。请只在确实会调用权重端点时等待服务。
Character GPT/SoVITS paths may be empty, and DefaultCharacterTtsHandler calls switch_model before checking preset. Empty paths resolve to the working directory and match neither weight suffix: the old code made no model request and played the clip, whereas this readiness wait raises while the server is down. Gate the wait on an actual weight switch.</issue_to_address>
2. 失败清理可能删除已落盘配置引用的音频 / Failure cleanup can delete audio already referenced on disk
config/character_manager.py:596 (RIGHT)
<issue_to_address>_save_single_config 使用 'w' 原地写入,因此 False 可能在文件已截断或新 YAML 已写入后才返回,例如写入或关闭时发生磁盘错误。这里随后删除新 voice_path,但只回滚内存;若 YAML 已写入新路径,重启后配置会指向已删除的音频(或配置已被截断)。基线在该失败场景保留复制文件,新的清理分支引入了这个悬空引用。应先原子发布 YAML,再把失败视为可安全清理的未提交状态。
_save_single_config writes in place with 'w', so it can return False after truncation or after the new YAML was written, such as on a write or close error. This deletes the new voice_path while only rolling back memory; a persisted new path then dangles after restart. The base path retained the copied file in this failure case, so the new cleanup introduces the dangling reference. Publish YAML atomically before treating failure as safe to clean up.</issue_to_address>
3. 持久化失败被桥接端报告为成功 / The bridge reports a failed save as a successful upload
config/character_manager.py:602 (RIGHT)
<issue_to_address>此分支返回失败消息和 None,但 frontend_bridge_core/characters.py 的 _upload_sprite_voice 只把四种旧消息前缀当作错误,并忽略返回路径;HTTP handler 随后仍以 200 返回。React 调用方进入 onSuccess,清除待上传路径并显示成功提示。比如目标 YAML 不可写时,用户会看到上传成功,但原语音并未更新。
This branch returns a failure message and None, but _upload_sprite_voice in frontend_bridge_core/characters.py recognizes only four older error prefixes and ignores the returned path; the HTTP handler still returns 200. The React caller then enters onSuccess, clears the pending upload, and shows success. For example, an unwritable YAML target appears to upload successfully while the original voice remains unchanged.</issue_to_address>
4. YAML 优先读取丢失既有 voice_type 大小写兼容性 / YAML-first lookup loses existing voice-type case compatibility
core/handlers/tts_message_handler.py:108 (RIGHT)
<issue_to_address>normalize_sprite_voice_types 在加载既有配置时会接受并标准化 REFERENCE 等大小写变体,旧路径从缓存读取该标准化值。这里的新 YAML 优先读取绕过了该步骤:只要 YAML 有 voice_path,就返回原始 REFERENCE,而后续条件仅接受精确的 reference 或 None,使有效的立绘参考音频退回角色默认音频。
normalize_sprite_voice_types accepts and normalizes existing case variants such as REFERENCE, and the old path read that normalized cached value. This YAML-first lookup bypasses that step: when YAML has a voice_path, it returns raw REFERENCE, while the later condition accepts only exact reference or None. Valid sprite reference audio then falls back to the character default.</issue_to_address>
| # Wait for the local server to accept connections before switching weights, | ||
| # so a transient startup/restart race retries briefly instead of failing the | ||
| # whole TTS turn with ConnectionRefusedError. | ||
| if not self._server_is_reachable(): |
There was a problem hiding this comment.
[MEDIUM · bug] 无权重切换时不应阻断预录语音 / Do not block preset audio for a no-op model switch
角色的 GPT/SoVITS 路径允许为空,而 DefaultCharacterTtsHandler 会先调用 switch_model,随后才检查 preset。空路径会解析为工作目录,且两个路径都不以 .ckpt/.pth 结尾:旧实现不会发起模型请求,直接播放预录音频;这里却在服务重启或离线时等待并抛出,导致可用的 preset 音频无法播放。请只在确实会调用权重端点时等待服务。
Original in English
Character GPT/SoVITS paths may be empty, and DefaultCharacterTtsHandler calls switch_model before checking preset. Empty paths resolve to the working directory and match neither weight suffix: the old code made no model request and played the clip, whereas this readiness wait raises while the server is down. Gate the wait on an actual weight switch.
置信度 98% · 可直接回复讨论 · 👍/👎 会影响后续审查
| sprite_data["voice_path"] = original_voice_path | ||
| sprite_data["voice_text"] = original_voice_text | ||
| sprite_data["voice_type"] = original_voice_type | ||
| if os.path.abspath(voice_path) != os.path.abspath(original_voice_path): |
There was a problem hiding this comment.
[MEDIUM · bug] 失败清理可能删除已落盘配置引用的音频 / Failure cleanup can delete audio already referenced on disk
_save_single_config 使用 'w' 原地写入,因此 False 可能在文件已截断或新 YAML 已写入后才返回,例如写入或关闭时发生磁盘错误。这里随后删除新 voice_path,但只回滚内存;若 YAML 已写入新路径,重启后配置会指向已删除的音频(或配置已被截断)。基线在该失败场景保留复制文件,新的清理分支引入了这个悬空引用。应先原子发布 YAML,再把失败视为可安全清理的未提交状态。
_save_single_config writes in place with 'w', so it can return False after truncation or after the new YAML was written, such as on a write or close error. This deletes the new voice_path while only rolling back memory; a persisted new path then dangles after restart. The base path retained the copied file in this failure case, so the new cleanup introduces the dangling reference. Publish YAML atomically before treating failure as safe to clean up.
置信度 94% · 可直接回复讨论 · 👍/👎 会影响后续审查
| os.remove(voice_path) | ||
| except OSError: | ||
| pass | ||
| return "语音保存失败:写入角色配置未成功,已保留原语音。", None |
There was a problem hiding this comment.
[LOW · bug] 持久化失败被桥接端报告为成功 / The bridge reports a failed save as a successful upload
此分支返回失败消息和 None,但 frontend_bridge_core/characters.py 的 _upload_sprite_voice 只把四种旧消息前缀当作错误,并忽略返回路径;HTTP handler 随后仍以 200 返回。React 调用方进入 onSuccess,清除待上传路径并显示成功提示。比如目标 YAML 不可写时,用户会看到上传成功,但原语音并未更新。
Original in English
This branch returns a failure message and None, but _upload_sprite_voice in frontend_bridge_core/characters.py recognizes only four older error prefixes and ignores the returned path; the HTTP handler still returns 200. The React caller then enters onSuccess, clears the pending upload, and shows success. For example, an unwritable YAML target appears to upload successfully while the original voice remains unchanged.
置信度 99% · 可直接回复讨论 · 👍/👎 会影响后续审查
| yaml_voice_type, yaml_voice_path, yaml_voice_text = _read_sprite_voice_cfg( | ||
| character_config.name, sprite_id | ||
| ) | ||
| voice_type = yaml_voice_type |
There was a problem hiding this comment.
[LOW · bug] YAML 优先读取丢失既有 voice_type 大小写兼容性 / YAML-first lookup loses existing voice-type case compatibility
normalize_sprite_voice_types 在加载既有配置时会接受并标准化 REFERENCE 等大小写变体,旧路径从缓存读取该标准化值。这里的新 YAML 优先读取绕过了该步骤:只要 YAML 有 voice_path,就返回原始 REFERENCE,而后续条件仅接受精确的 reference 或 None,使有效的立绘参考音频退回角色默认音频。
normalize_sprite_voice_types accepts and normalizes existing case variants such as REFERENCE, and the old path read that normalized cached value. This YAML-first lookup bypasses that step: when YAML has a voice_path, it returns raw REFERENCE, while the later condition accepts only exact reference or None. Valid sprite reference audio then falls back to the character default.
| voice_type = yaml_voice_type | |
| voice_type = str(yaml_voice_type or "").strip().lower() or None |
置信度 98% · 可直接回复讨论 · 👍/👎 会影响后续审查
… model switch
Note: "default reference synthesis" = cloning with the character-level refer_audio_path; distinct from the voice_type=="fallback" pre-recorded clip that only plays when no TTS engine is configured.
Summary by Ameath
小爱先把这次核对的重点轻轻收拢,方便你顺着往下看。
该 PR 改进了立绘语音替换、失效参考音频回退和 GPT-SoVITS 服务就绪等待,并补充了相关单元测试;审查发现预录语音可用性、失败持久化及既有配置兼容性回归。
Enhancements
characters.yaml并检查参考音频是否存在,失效时回退角色级参考音频。Tests
Original summary in English
This PR improves sprite-voice replacement, fallback from missing reference audio, and GPT-SoVITS readiness waiting, with related unit tests. The review found regressions in prerecorded-audio availability, failed persistence, and existing-config compatibility.
Enhancements
characters.yamlfirst at runtime and checks reference-audio existence before falling back to the character-level reference audio.Tests