Repository navigation
Conversation
朗读请求收敛为 ChatViewModel 内单一串行消费者:speakMessage 只入队 (SpeakRequest Channel + 代数校验),常驻协程逐条处理后 join, 保证 provider.speak 的调用顺序与 flush 顺序一致,消除并发协程导致的 跳句、乱序与丢失;QueuedTtsPlayback 与 SiliconFlowVoiceProvider 在合成 成功即完成,保留队列型 Provider 的句间预合成流水线。 相对本地分支的合并差异:清理链沿用主干既有的 TtsCleaner.cleanForSpeech(extractReplyBody 剥离协议 meta/思考链/tool 块) 而不是本地实现里的 cleanMessageContentForCopy 叠加,避免同一内容被两套 过滤器重复裁剪。
CATMIAOZHI
self-requested a review
November 13, 2026 07:35
CATMIAOZHI
reviewed
Nov 13, 2026
CATMIAOZHI
left a comment
Collaborator
There was a problem hiding this comment.
审计结论:💬 意见
改动(4 个文件,+247/−75):自动朗读收敛为 ChatViewModel 内的单一串行消费者(SpeakRequest + UNLIMITED Channel + 代数防残留),队列型 Provider 的 speak 返回时机提前到"合成完成、已进入播放队列",保留句间预合成流水线。
通过的检查:
- 目标分支为
dev,非草稿 PR,单提交,diff 与 PR 描述一致。 - 串行化根因找得准:此前每次 flush 起独立协程、
speechPlaybackJob反复被覆盖且旧协程不取消,提交顺序取决于线程调度;收敛到调用方单点串行是正确位置(Provider 侧加锁恢复不了已丢失的 flush 顺序)。 - 中断语义三路调用方都已核对:流式自动朗读仅首句
interrupt=true(MessageProcessingDelegate.tryFlushAutoRead)、waifu 分段仅首段或 Retain 截断后为 true(WaifuSegmentMessageStore)、收尾整条朗读受skipFinalAutoRead保护;因此串行消费者的"interrupt 提升代数 + 取消当前播放 + 清空积压"不会在句间误触发。 - 代数双重校验覆盖了
Main.immediate下子协程内联启动先于speechPlaybackJob赋值的时间窗;stopSpeaking先升代数再取消、清空,与 provider 侧stopGeneration是同一模式。 processSpeakRequest内吞掉CancellationException只记日志是必要的:若向上抛,playbackJob.join()会把取消带进消费者循环,导致常驻消费者永久退出;与旧版协程体行为一致。- 无死锁:
SimpleVoiceProvider.speak提交 utterance 后即返回(不等播完),串行join只串行化提交顺序;队列型 Provider 靠 prepare 循环预合成,串行等待不造成句间空窗。 QueuedTtsPlayback的改动无行为回归:旧版playPreparedRequest本就无条件返回 true(playAudioFile内部吞异常),播放失败本来也不会回传给调用方。- 无 strings 变更、无数据格式/API/Room 迁移;编译作者已验证通过(
compileDebugKotlinexit 0)。
问题:
- [P2] SiliconFlow 的播放阶段失败现在对调用方完全不可见。旧版播放循环把
playPreparedRequest的返回值(即playAudioFileAndAwait的结果,空文件/MediaPlayer 异常时为 false)回传给speak()调用方,ChatViewModel会据此打segmentFailed、弹"朗读失败" toast 并中断后续分段,TTS 测试页也会显示失败。新版在合成完成时即complete(true),播放结果直接丢弃、只记日志。影响面:TextToSpeechScreen的失败提示对 SiliconFlow 播放失败不再可达;设置里的 TTS 测试工具(StandardSoftwareSettingsModifyTools)的playbackTriggered/success 现在含义变为"已合成入队"而非"已播放",PR 描述未提及这个调用方。建议:至少在 KDoc/文档里明确这一语义变化;若希望保留可观测性,可记录最近一次播放失败并在日志/测试工具中暴露。 - [P2] 新增实施记录
docs/TODO/tts_auto_read_serial_playback_20261107/index.md的"步骤 3 [DONE]"与 diff 不符:文档称朗读清理链插入了cleanMessageContentForCopy、修复了思考链大小写/属性变体(<Think>、<think >)并补齐 OpenAI Responses meta、附件与内部媒体链接的整块移除,涉及文件列为ChatViewModel.kt;但实际 diff 未改动任何清理链文件,ChatViewModel.kt里也没有cleanMessageContentForCopy。所描述的过滤能力(思考链变体、OpenAI meta 等)是主干TtsCleaner.extractReplyBody已有的行为,并非本 PR 引入;PR 描述"不叠加第二套过滤器"与文档步骤 3 互相矛盾。请修正文档(去掉步骤 3,或改为"复核:主干已有,无需改动"),避免后人误以为本 PR 修了思考链朗读问题。同理,"已知边界"里"播放阶段错误本就只记日志"的说法对 SiliconFlow 不准确(见问题 1,旧版是会回传 false 的)。 - [P3]
VoiceService.speak接口 KDoc 仍沿用旧契约(只写"操作是否成功"),未定义成功到底指"提交成功"、"合成并入队成功"还是"播放完成"。两个队列型 Provider 的 KDoc 已更新,建议同步更新接口 KDoc,明确各实现的返回语义,避免后续调用方(例如测试工具)按旧理解使用返回值。 - [question] PR 描述中手工回归标注"结果待补",单元测试未在本机执行、交由 CI。自动朗读三条核心回归(
autoRead[cut]/speech[segmentSpeak]/queue[speak.submitted]序列单调一致、朗读中发新消息旧轮不插播、暂停后继续按序恢复)是否已在真机上跑过?若尚未跑,建议补上结果后再合入,因为串行化+返回时机提前这两处改动的正确性主要靠这几条回归覆盖,CI 的编译检查覆盖不到。
后续可跟进(不阻塞):
- 悬浮窗朗读路径(
SpeechInteractionManager每次 speak 起独立协程)存在同类的并发提交模式,文档已注明不在本次作用域,可后续另开 PR 处理。
由 水晴喵的muse 审计
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
变更说明 / Description
背景与动机 / Context and motivation
自动朗读路径上,流式收集器每凑满一句(TtsSegmenter 按句末标点或 50 字切分)就调用一次 ChatViewModel.speakMessage,而 speakMessage 每次都起一个全新的播放协程,speechPlaybackJob 引用被反复覆盖、旧协程不取消继续运行:一次流式回复会产生几十个并发播放协程。协程体内对 SimpleVoiceProvider(系统 TTS)的 speak 调用经 withContext(Dispatchers.IO) 分发后由线程池并发执行,锁外的 ensureVoiceAndLocaleReady 与 synchronized 内的提交顺序取决于线程调度,句子进入系统 TTS 队列的顺序与 flush 顺序不一致,造成自动朗读跳句、乱序、插队或部分句子丢失。手动朗读是单协程顺序提交,不受影响。
队列型 Provider(QueuedTtsPlayback:HttpVoiceProvider、Doubao;SiliconFlowVoiceProvider 同构队列)的 speak 在播放完成后才返回,原实现依赖并发协程实现"当前句播放时下一句预合成"的流水线,因此不能简单地把调用改成串行等待。
改动范围 / Changes
不包含:MiniMax/Mimo 委托 HttpVoiceProvider(内部即 QueuedTtsPlayback),随本改动一并生效,无需单独修改;悬浮窗 SpeechInteractionManager 为 fire-and-forget 调用,不受返回时机变化影响;清理链本身不在本 PR 范围内改动。
兼容性与风险 / Compatibility and risks
关联 Issue / Related issue
N/A。背景与实施记录见 docs/TODO/tts_auto_read_serial_playback_20261107/index.md。
验证方式 / Verification
手工回归(计划记录于 TODO 文档,结果待补):自动朗读日志中 autoRead[cut] 序列与 speech[segmentSpeak] 序列、SimpleVoiceProvider queue[speak.submitted] 序列单调一致;朗读中发送新消息,旧朗读立即停止且旧轮积压句子不再插播;暂停后继续,句子按序恢复不并发涌出;队列型 Provider 句间无可感知合成空窗。
检查清单 / Checklist
dev;如目标为main,我已说明这是维护者发布同步 / The target branch isdevfor regular work; if it ismain, I explained why this is a maintainer-led release syncCandidate checks覆盖改动范围,并会处理技术失败项 /Candidate checkscovers the change scope and technical failures will be addressed