Skip to content

feat(character): add scenario voice trigger tags#181

Open
V1per-cmyk wants to merge 5 commits into
RachelForster:mainfrom
V1per-cmyk:feat/character-emotion-module
Open

feat(character): add scenario voice trigger tags#181
V1per-cmyk wants to merge 5 commits into
RachelForster:mainfrom
V1per-cmyk:feat/character-emotion-module

Conversation

@V1per-cmyk

@V1per-cmyk V1per-cmyk commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

功能说明

在角色编辑页「立绘管理」和「长期记忆」之间新增语音触发标签模块。

AI 说话时可以通过 [tagname] 触发角色预设的情景语音,例如 [sad] 匹配到角色 YAML 中配好的 sad 情景,自动切换对应的语音/参考音频。

语音优先级

  1. [tagname] 语音触发标签 — 最高优先。preset 直接播放音频;reference 覆盖 TTS 参考音频
  2. 立绘的 voice_text/voice_path — 第二优先
  3. 角色级 refer_audio_path — 兜底默认参考语音

主要改动

模块 改动
前端 新增 CharacterScenarioSection 组件、接口类型、i18n
TTS Handler 新增 _match_voice_tag 标签检测、_read_scenarios_from_yaml 读取情景配置
LLM 模板 语音标签注入到系统提示词
角色管理 新增 scenarios 的 CRUD
前端桥接 新增 scenario 上传/保存/删除接口
防御修复 voice_type 小写归一化、立绘 dict/Pydantic 兼容

测试

  • 相关测试 64 passed, 0 failed

🤖 Generated with Claude Code
Co-Authored-By: Claude noreply@anthropic.com

Summary by Ameath

小爱先把这次核对的重点收拢一下,方便继续判断后续处理。

该 PR 在角色编辑、桥接、LLM 模板和 TTS 链路中加入语音触发标签;静态审查发现前端编译、标签执行及跨保存路径持久化方面存在需要修正的问题。

New Features

  • 新增 CharacterScenario 配置、角色编辑区和场景语音桥接接口。
  • LLM 模板可注入角色标签,TTS 可按 [tag] 选择预设音频或参考音频。

Enhancements

  • 增加 voice_type 小写归一化及立绘字典/Pydantic 兼容处理。

Tests

  • 更新角色草稿默认 scenarios 断言及平台仓储 mock。

Chores

  • 新增前端 pnpm-workspace.yaml 的 esbuild 构建许可配置。
Original summary in English

This PR adds voice-trigger tags across the character editor, bridge, LLM template, and TTS pipeline; the static review found issues in frontend compilation, tag execution, and persistence across save paths.

New Features

  • Adds CharacterScenario configuration, editor UI, and bridge APIs for scenario voices.
  • Lets the LLM template inject character tags and lets TTS select preset or reference audio for [tag].

Enhancements

  • Adds lowercase voice_type normalization and dictionary/Pydantic sprite compatibility.

Tests

  • Updates default-character-draft scenarios assertions and platform repository mocks.

Chores

  • Adds the frontend pnpm-workspace.yaml esbuild build allowlist.

V1per-cmyk and others added 4 commits June 14, 2026 18:43
- 新增 CharacterScenario 数据模型,替代旧的 emotion→action 嵌套结构
- 前端新增 CharacterScenarioSection 组件,位于立绘管理和长期记忆之间
- 每个情景支持:名称、语音上传(预设/参考)、语音文本、AI 翻译标签名
- TTS handler 新增 [tagname] 触发匹配逻辑
- 支持 preset 跳过 TTS / reference 作为参考音色
- 新增场景 CRUD API: /api/characters/scenarios/*
- 新增 i18n 键值(zh_CN/en/ja)

Co-Authored-By: Claude <noreply@anthropic.com>
- _read_scenarios_from_yaml 对 scenario 的 voice_type 做 strip().lower()
- 立绘语音读取兼容 dict 和 Pydantic Sprite 两种类型

Co-Authored-By: Claude <noreply@anthropic.com>
@End0rph1nww

Copy link
Copy Markdown
Collaborator

@Ame4th review

@ameath-review

ameath-review Bot commented Jul 12, 2026

Copy link
Copy Markdown

Reviewer's Guide by Ameath

小爱会沿着关键路径往下看,优先留意容易影响结果的细节。

审查聚焦 CharacterScenario 从 React 草稿、platform adapter、bridge/manager 到 characters.yaml、角色包导入导出和 TTS/UI 消费链的完整路径。

  • 优先恢复 File[] 导入兼容性及 TTS 的 YAML/翻译标签匹配。
  • 随后修复 TemplateGenerator 的角色名约束覆写,避免 LLM 输出协议自相矛盾。
  • 最后处理新角色首次保存、角色包往返、局部突变回滚和场景音频生命周期的一致性。

文件级变更

Change Details Files
新增文件 2 个文件,+187 / -0 frontend/pnpm-workspace.yaml
frontend/src/features/character-editor/CharacterScenarioSection.tsx
修改文件(1/3) 10 个文件,+565 / -32 config/character_config.py
config/character_manager.py
config/schema.py
core/handlers/tts_message_handler.py
core/handlers/ui_message_handler.py
frontend/src/entities/character/repository.ts
frontend/src/entities/config/types.ts
frontend/src/features/character-editor/CharacterEditorPage.css
frontend/src/features/character-editor/CharacterEditorPage.tsx
frontend/src/features/character-editor/characterEditorUtils.ts
修改文件(2/3) 10 个文件,+208 / -1 frontend/src/shared/i18n/messages.ts
frontend/src/shared/i18n/messages/en.ts
frontend/src/shared/i18n/messages/ja.ts
frontend/src/shared/i18n/messages/zh_CN.ts
frontend/src/shared/platform/browserPreviewPlatform.ts
frontend/src/shared/platform/httpPlatform.ts
frontend/src/shared/platform/types.ts
frontend/src/test/characterEditorUtils.test.ts
frontend/src/test/features/character-editor/characterEditorUtils.test.ts
frontend/src/test/repositories.test.ts
修改文件(3/3) 6 个文件,+181 / -6 frontend_bridge_core/characters.py
frontend_bridge_core/handler.py
i18n/locales/en.json
i18n/locales/ja.json
i18n/locales/zh_CN.json
llm/template_generator.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 follows CharacterScenario end-to-end: React draft, platform adapter, bridge/manager, characters.yaml, character package import/export, and TTS/UI consumers.

  • Restore File[] import compatibility and YAML/translated-speech tag matching first.
  • Then fix the TemplateGenerator character-name constraint overwrite so the LLM output contract is not contradictory.
  • Finally address consistency for first-save creation, character package round trips, partial-mutation rollback, and scenario-audio lifecycle.

@ameath-review ameath-review Bot changed the title feat: 语音触发标签模块 feat(character): add scenario voice trigger tags Jul 12, 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.

小爱把需要进一步确认的地方标出来了,咱们逐处看看。

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

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

风险

  • PR 元数据未关联 issue;基线 CONTRIBUTING.md 要求每个 PR 在说明中链接现有 issue。
  • 场景名 AI 翻译将提示构造、LLM adapter 创建和 JSON 解析放在 frontend_bridge_core/characters.py;这偏离基线的薄 bridge 模块边界。
  • 变更包含 28 个文件和 1,141 行新增,超过贡献指南“数百行时考虑拆分”的范围提示。

建议测试

  • 补充 CharacterSetupPanel 通过文件选择器传入 File[] 的类型与平台上传路径覆盖。
  • 补充 TemplateGenerator 多角色场景测试,覆盖最后角色有/无标签时 r_cname 仍列出所选角色。
  • 补充 DefaultCharacterTtsHandler 从 YAML 加载 preset/reference 标签的覆盖,并验证含 translate 的台词、连字符标签和句中标签空格。
Prompt for AI Agents

请逐项处理以下审查问题;修改前先核实上下文,修改后运行相关测试。

1. 让角色包往返保存场景 / Preserve scenarios in character package round trips

config/character_config.py:69 (RIGHT)
<issue_to_address>角色包是该配置的持久化消费者:桥接导出会先构造 CharacterConfig,但 tools/file_util.export_character 建立的 char_data 没有 scenarios,导入端也不会重写其 voice_path。导出带标签角色时,整个 speech 目录会被复制,但导入后没有任何场景引用这些音频,标签配置被静默丢失。

Character packages consume this configuration. The exporter omits scenarios from char_data and the importer has no scenario voice-path rebasing, so exporting then importing a tagged character loses its tag configuration while copying orphaned audio.</issue_to_address>

2. 不要用可变索引命名场景音频 / Do not name scenario audio by mutable index

config/character_manager.py:695 (RIGHT)
<issue_to_address>数组索引不是稳定的场景身份。保存两个 .wav 标签后,删除第一个并保存会让原第二个标签保留 scenario_01.wav;再添加一个位于索引 1 的标签并上传 .wav,此处会覆盖同一文件,两个标签随后播放新音频。应使用稳定场景 ID 或不可复用的文件名。

An array index is not stable scenario identity. After deleting an earlier tag, a newly added tag can reuse the old index and overwrite another surviving tag's audio file.</issue_to_address>

3. 不要覆盖角色名约束变量 / Do not overwrite the character-name constraint

llm/template_generator.py:377 (RIGHT)
<issue_to_address>外层 names 原本是已选角色名的字符串;此循环将它重绑定为最后一个角色的情景标签列表。后面的 r_cname 要求仍使用该变量:最后角色没有标签时会写入 [],有标签时会把标签当作允许的角色名,和前面的字段约束形成矛盾并损坏生成对话的协议。

The outer names holds selected character names, but this block rebinds it to scenario tags. The later r_cname requirement therefore receives [] or tag names instead of character names, corrupting the generated-dialog contract.</issue_to_address>

4. 将场景音频与立绘清理生命周期隔离 / Isolate scenario audio from sprite cleanup

config/character_manager.py:692 (RIGHT)
<issue_to_address>现有 delete_all_sprites 会递归删除 VOICE_DIR/<sprite_prefix>,但只清空立绘和情绪标签,不清空 character.scenarios。把新情景音频放入该目录后,“清空全部立绘”会删除 preset/reference 文件却在 YAML 中保留 voice_path,下一次标签触发将使用不存在的音频或参考文件。

delete_all_sprites recursively removes VOICE_DIR/<sprite_prefix> without clearing scenarios. Storing scenario audio there makes Clear all sprites delete configured audio while YAML retains stale paths.</issue_to_address>

5. 不要用陈旧服务器快照覆盖场景草稿 / Do not overwrite scenario drafts with stale server snapshots

frontend/src/features/character-editor/CharacterEditorPage.tsx:405 (RIGHT)
<issue_to_address>名称和语音文本编辑只存在于本地草稿,直到“保存所有标签”。用户随后切换语音类型或上传音频时,局部 API 只基于服务器旧版本更新一个字段,而这里用其返回的完整 scenarios 覆盖草稿,导致未保存的名称/文本回滚;新加但未保存的标签还会因服务器没有该索引而失败。

Name and voice-text edits remain local until Save all. A type change or upload returns a server snapshot based on the old list, and this replacement discards those unsaved edits; a newly added unsaved index fails on the server.</issue_to_address>

6. 首次保存不能丢失场景草稿 / Preserve scenarios on the initial character save

frontend/src/features/character-editor/CharacterEditorPage.tsx:1050 (RIGHT)
<issue_to_address>新建角色时此入口可用,且主保存会提交 draft.scenarios;但 /api/characters_save_character 没有将该字段传给 CharacterManager.add_character,其新建分支也未设置 scenarios。因此“新建角色 → 添加标签 → 保存角色”会显示成功后用空列表回填草稿;browser preview 保留该字段,形成 live/snapshot 不一致。

This control is available while creating a character, but the live /api/characters save drops scenarios when constructing a new character. Creating a character, adding a tag, and saving returns success with the tags lost, unlike browser preview.</issue_to_address>

7. 统一标签语法并保留词间空格 / Align tag grammar and preserve word spacing

core/handlers/tts_message_handler.py:64 (RIGHT)
<issue_to_address>UI 和 schema 未限制标签名只能是 \w+,因此可保存的 sad-face 会被模板列出却永远无法由该正则匹配。并且同一清理逻辑中的 \s* 会同时吞掉标签两侧空白,使 I feel [sad] very tired 变成 I feelvery tired;后续 TTS/UI 清理也重复该行为。应统一标签语法校验与清理规则,并保留词间分隔。

The UI/schema allow names beyond \w+, so a saved sad-face tag never matches. The surrounding \s* also joins words around mid-sentence tags, and the later TTS/UI cleaners repeat it.</issue_to_address>

8. 翻译台词仍需从原文匹配标签 / Match tags from source speech when translation is enabled

core/handlers/tts_message_handler.py:189 (RIGHT)
<issue_to_address>模板规则要求把标签写入 speech,而 translate 是独立的 TTS 目标语言文本。启用 LLM 翻译时,代码在匹配前已用 translate 覆盖 speech_text;合法输出如 speech: "[sad]..."、不含标签的 translate 因而永远不会触发情景语音。应从原始 speech 识别标签,再分别清理用于 TTS 的翻译文本。

The template places tags in speech, but translation replaces speech_text before matching. A valid tagged source line with an untagged translation never activates preset or reference audio.</issue_to_address>

9. 翻译失败时不要显示成功 / Do not report success when translation failed

frontend/src/features/character-editor/CharacterEditorPage.tsx:431 (RIGHT)
<issue_to_address>桥接在 LLM 未配置时返回 { "translated": {}, "error": "LLM not configured" },并且会把 adapter/JSON 失败吞成空翻译;该 mutation 随后只返回本地 updated 数组,所以这里无条件显示成功。点击“AI 翻译”会在没有任何翻译结果时误报成功。

The bridge returns an error payload for an unconfigured LLM and suppresses adapter/JSON failures into an empty result. This mutation discards that status and always shows success, even when no tag was translated.</issue_to_address>

10. 先提交元数据再删除场景音频 / Commit metadata before deleting scenario audio

config/character_manager.py:722 (RIGHT)
<issue_to_address>已注册的 /api/characters/scenarios/voice-delete 会先删除文件、后保存 YAML。若进程在两步之间退出,characters.yaml 仍指向已不存在的 preset/reference 文件;下次触发只能得到无音频或失败的参考路径。现有 delete_sprite_voice 先持久化清空引用,情景删除应采用同样顺序或可恢复的事务。

The registered voice-delete route unlinks the file before persisting YAML. A crash in between leaves a stale preset/reference path; the existing sprite-voice deletion persists the cleared reference first.</issue_to_address>

Comment thread config/character_config.py
Comment thread config/character_manager.py Outdated
voice_dir = os.path.join(VOICE_DIR, character.sprite_prefix)
Path(voice_dir).mkdir(parents=True, exist_ok=True)
file_ext = Path(voice_file).suffix
voice_filename = f"scenario_{scenario_index:02d}{file_ext}"

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 name scenario audio by mutable index

数组索引不是稳定的场景身份。保存两个 .wav 标签后,删除第一个并保存会让原第二个标签保留 scenario_01.wav;再添加一个位于索引 1 的标签并上传 .wav,此处会覆盖同一文件,两个标签随后播放新音频。应使用稳定场景 ID 或不可复用的文件名。

Original in English

An array index is not stable scenario identity. After deleting an earlier tag, a newly added tag can reuse the old index and overwrite another surviving tag's audio file.

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

Comment thread llm/template_generator.py Outdated
Comment on lines +367 to +377
names = []
for s in scenarios:
sn = s.name if hasattr(s, 'name') else s.get('name', '')
if sn:
names.append(sn)
if names:
voice_tags.append((char_name, names))
if voice_tags:
template += _T("voice_tags_header")
for char_name, names in voice_tags:
template += _T("voice_tags_for", name=char_name, tags=", ".join(names))

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 · correctness] 不要覆盖角色名约束变量 / Do not overwrite the character-name constraint

外层 names 原本是已选角色名的字符串;此循环将它重绑定为最后一个角色的情景标签列表。后面的 r_cname 要求仍使用该变量:最后角色没有标签时会写入 [],有标签时会把标签当作允许的角色名,和前面的字段约束形成矛盾并损坏生成对话的协议。

Original in English

The outer names holds selected character names, but this block rebinds it to scenario tags. The later r_cname requirement therefore receives [] or tag names instead of character names, corrupting the generated-dialog contract.

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

if not voice_file:
return "请选择语音文件!", None

voice_dir = os.path.join(VOICE_DIR, character.sprite_prefix)

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] 将场景音频与立绘清理生命周期隔离 / Isolate scenario audio from sprite cleanup

现有 delete_all_sprites 会递归删除 VOICE_DIR/<sprite_prefix>,但只清空立绘和情绪标签,不清空 character.scenarios。把新情景音频放入该目录后,“清空全部立绘”会删除 preset/reference 文件却在 YAML 中保留 voice_path,下一次标签触发将使用不存在的音频或参考文件。

delete_all_sprites recursively removes VOICE_DIR/<sprite_prefix> without clearing scenarios. Storing scenario audio there makes Clear all sprites delete configured audio while YAML retains stale paths.

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

saveScenarioVoiceType({ name: currentCharacterName, ...input }),
onSuccess(character) {
queryClient.invalidateQueries({ queryKey: charactersQueryKey });
setDraft((current) => ({ ...current, scenarios: character.scenarios }));

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 · correctness] 不要用陈旧服务器快照覆盖场景草稿 / Do not overwrite scenario drafts with stale server snapshots

名称和语音文本编辑只存在于本地草稿,直到“保存所有标签”。用户随后切换语音类型或上传音频时,局部 API 只基于服务器旧版本更新一个字段,而这里用其返回的完整 scenarios 覆盖草稿,导致未保存的名称/文本回滚;新加但未保存的标签还会因服务器没有该索引而失败。

Original in English

Name and voice-text edits remain local until Save all. A type change or upload returns a server snapshot based on the old list, and this replacement discards those unsaved edits; a newly added unsaved index fails on the server.

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

isSavedCharacter={isSavedCharacter}
scenarioSavePending={scenarioSaveMutation.isPending}
translatePending={scenarioTranslateMutation.isPending}
onAddScenario={scenarioHandlers.addScenario}

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] 首次保存不能丢失场景草稿 / Preserve scenarios on the initial character save

新建角色时此入口可用,且主保存会提交 draft.scenarios;但 /api/characters_save_character 没有将该字段传给 CharacterManager.add_character,其新建分支也未设置 scenarios。因此“新建角色 → 添加标签 → 保存角色”会显示成功后用空列表回填草稿;browser preview 保留该字段,形成 live/snapshot 不一致。

Original in English

This control is available while creating a character, but the live /api/characters save drops scenarios when constructing a new character. Creating a character, adding a tag, and saving returns success with the tags lost, unlike browser preview.

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

if not speech or not scenarios:
return None, speech
tags = re.findall(r'\[(\w+)\]', speech)
if not tags:

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 · correctness] 统一标签语法并保留词间空格 / Align tag grammar and preserve word spacing

UI 和 schema 未限制标签名只能是 \w+,因此可保存的 sad-face 会被模板列出却永远无法由该正则匹配。并且同一清理逻辑中的 \s* 会同时吞掉标签两侧空白,使 I feel [sad] very tired 变成 I feelvery tired;后续 TTS/UI 清理也重复该行为。应统一标签语法校验与清理规则,并保留词间分隔。

Original in English

The UI/schema allow names beyond \w+, so a saved sad-face tag never matches. The surrounding \s* also joins words around mid-sentence tags, and the later TTS/UI cleaners repeat it.

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

_tag_ref_audio = None
_tag_prompt = None
_scenarios = _read_scenarios_from_yaml(name_s)
_matched, _clean_speech = _match_voice_tag(speech_text, _scenarios)

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 · correctness] 翻译台词仍需从原文匹配标签 / Match tags from source speech when translation is enabled

模板规则要求把标签写入 speech,而 translate 是独立的 TTS 目标语言文本。启用 LLM 翻译时,代码在匹配前已用 translate 覆盖 speech_text;合法输出如 speech: "[sad]..."、不含标签的 translate 因而永远不会触发情景语音。应从原始 speech 识别标签,再分别清理用于 TTS 的翻译文本。

Original in English

The template places tags in speech, but translation replaces speech_text before matching. A valid tagged source line with an untagged translation never activates preset or reference audio.

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

queryClient.invalidateQueries({ queryKey: charactersQueryKey });
setDraft((current) => ({ ...current, scenarios: updated }));
}
showToast({ kind: "success", title: t("character.voiceTag.aiTranslate") });

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] 翻译失败时不要显示成功 / Do not report success when translation failed

桥接在 LLM 未配置时返回 { "translated": {}, "error": "LLM not configured" },并且会把 adapter/JSON 失败吞成空翻译;该 mutation 随后只返回本地 updated 数组,所以这里无条件显示成功。点击“AI 翻译”会在没有任何翻译结果时误报成功。

Original in English

The bridge returns an error payload for an unconfigured LLM and suppresses adapter/JSON failures into an empty result. This mutation discards that status and always shows success, even when no tag was translated.

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

Comment thread config/character_manager.py Outdated
scenario = character.scenarios[scenario_index]
if scenario.voice_path:
try:
os.remove(scenario.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.

[LOW · bug] 先提交元数据再删除场景音频 / Commit metadata before deleting scenario audio

已注册的 /api/characters/scenarios/voice-delete 会先删除文件、后保存 YAML。若进程在两步之间退出,characters.yaml 仍指向已不存在的 preset/reference 文件;下次触发只能得到无音频或失败的参考路径。现有 delete_sprite_voice 先持久化清空引用,情景删除应采用同样顺序或可恢复的事务。

Original in English

The registered voice-delete route unlinks the file before persisting YAML. A crash in between leaves a stale preset/reference path; the existing sprite-voice deletion persists the cleared reference first.

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

- (RachelForster#7) 标签清理正则保留词间空格,避免词汇粘连
- (RachelForster#8) 语音标签匹配移到翻译处理之前,翻译模式不再失效
- (RachelForster#3) template_generator 内循环变量 names→tag_names,不覆盖角色名
- (RachelForster#1) 角色包导出导入保留 scenarios 及 voice_path 重写
- (RachelForster#5) voiceType/voiceText 保存后只合并服务器字段,保留本地未存编辑
- (RachelForster#6) _save_character 创建角色后保存 scenarios
- (RachelForster#9) AI翻译检查服务器error字段,无结果时提示而非误报成功
- (RachelForster#2) 场景音频文件名从索引改为场景名,避免删标签后覆盖
- (RachelForster#4) delete_all_sprites 同步清空 scenarios,避免残留失效路径
- (RachelForster#10) delete_scenario_voice 先存YAML再删文件

Co-Authored-By: Claude <noreply@anthropic.com>
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.

3 participants