Skip to content

fix(tts): fall back from missing sprite references and await GPT-SoVITS readiness#248

Open
Mizushima-Mihane wants to merge 2 commits into
RachelForster:mainfrom
Mizushima-Mihane:fix/tts-voice-fallback
Open

fix(tts): fall back from missing sprite references and await GPT-SoVITS readiness#248
Mizushima-Mihane wants to merge 2 commits into
RachelForster:mainfrom
Mizushima-Mihane:fix/tts-voice-fallback

Conversation

@Mizushima-Mihane

@Mizushima-Mihane Mizushima-Mihane commented Jul 20, 2026

Copy link
Copy Markdown
Contributor

… 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.

Summary by Ameath

小爱先把这次核对的重点轻轻收拢,方便你顺着往下看。

该 PR 改进了立绘语音替换、失效参考音频回退和 GPT-SoVITS 服务就绪等待,并补充了相关单元测试;审查发现预录语音可用性、失败持久化及既有配置兼容性回归。

Enhancements

  • 为立绘语音文件名加入内容摘要,避免 GPT-SoVITS 按路径复用旧参考特征。
  • 运行时优先读取 characters.yaml 并检查参考音频是否存在,失效时回退角色级参考音频。
  • 在切换 GPT-SoVITS 权重前等待本地服务就绪。

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

  • Adds a content digest to sprite voice filenames to avoid GPT-SoVITS reusing cached reference features by path.
  • Reads characters.yaml first at runtime and checks reference-audio existence before falling back to the character-level reference audio.
  • Waits for the local GPT-SoVITS service before switching weights.

Tests

  • Adds unit tests for voice content digests, save-failure rollback, and service-ready/timeout paths.

… 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.
@ameath-review

ameath-review Bot commented Jul 20, 2026

Copy link
Copy Markdown

Reviewer's Guide by Ameath

小爱整理了一条清晰的查看路径,我们从关键处慢慢对齐。

审查重点覆盖 config/character_manager.py 的复制与持久化顺序及其 frontend_bridge_core/characters.py 调用方、core/handlers/tts_message_handler.py 对 YAML 与缓存立绘配置的解析,以及 tts/tts_adapter.pyDefaultCharacterTtsHandler 调用链中的就绪等待。发现的问题涉及已有 voice_type 大小写兼容、写入失败时的原子性与错误传播,以及无权重可切换时对预录 preset 语音的阻断。

文件级变更

Change Details Files
修改文件 7 个文件,+291 / -36 config/character_manager.py
config/config_manager.py
core/handlers/tts_message_handler.py
test/unit/adapters/test_gpt_sovits_adapter.py
test/unit/config/test_character_manager.py
test/unit/managers/test_character_manager.py
tts/tts_adapter.py

Tips and commands
  • 重新审查: @Ame4th review
  • 重新生成摘要: @Ame4th summary
  • 重新生成指南: @Ame4th guide
  • 生成标题: @Ame4th title
  • 解决本机器人线程: @Ame4th resolve
  • 清理本机器人审查: @Ame4th dismiss
  • 从行级评论创建 Issue: 回复 @Ame4th issue
  • 继续讨论: 直接回复任一行级评论。
  • 反馈偏好: 对行级评论点 👍 或 👎。
Original review guide in English

The review focused on copy/persistence ordering in config/character_manager.py and its frontend_bridge_core/characters.py caller, YAML-versus-cache sprite configuration resolution in core/handlers/tts_message_handler.py, and readiness waiting in the DefaultCharacterTtsHandler call chain through tts/tts_adapter.py. The findings concern existing voice_type case compatibility, atomicity and error propagation on write failure, and blocking prerecorded preset audio when no weights can be switched.

@ameath-review ameath-review Bot changed the title fix(tts): sprite-voice fallback, content-hash filenames, server-ready… fix(tts): fall back from missing sprite references and await GPT-SoVITS readiness Jul 20, 2026

@ameath-review ameath-review Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

小爱把需要留意的地方标出来了,看看这些细节是否符合预期。

小爱已完成全部变更审查,共发布 4 条行级建议(高风险 0,中风险 2)。

合并前关注点(风险 1 · 建议测试 1)

风险

  • PR 元数据未列出关联 issue;CONTRIBUTING.md 要求每个 PR 链接既有 issue。

建议测试

  • DefaultCharacterTtsHandler 的回退路径缺少覆盖:当 YAML 中的 sprite voice_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,而后续条件仅接受精确的 referenceNone,使有效的立绘参考音频退回角色默认音频。

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>

Comment thread tts/tts_adapter.py
# 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():

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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):

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[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

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[LOW · bug] YAML 优先读取丢失既有 voice_type 大小写兼容性 / YAML-first lookup loses existing voice-type case compatibility

normalize_sprite_voice_types 在加载既有配置时会接受并标准化 REFERENCE 等大小写变体,旧路径从缓存读取该标准化值。这里的新 YAML 优先读取绕过了该步骤:只要 YAML 有 voice_path,就返回原始 REFERENCE,而后续条件仅接受精确的 referenceNone,使有效的立绘参考音频退回角色默认音频。

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.

Suggested change
voice_type = yaml_voice_type
voice_type = str(yaml_voice_type or "").strip().lower() or None

置信度 98% · 可直接回复讨论 · 👍/👎 会影响后续审查

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants