Files
Cyrene-For-Android/app
AskaEth 222f691b78 fix(w75): 让「停止朗读」真的能停,切走不再补响旧朗读,缓存键带上 baseUrl
修 rev-w75 的三条发现(M5 / W75 TTS 朗读)。

P1-1 「停止朗读是空操作」—— 根因是三处合谋,谁都没错、合起来没反应:
MessageBubble 在播放中画暂停符、读屏名字是 TTS_STOP_DESCRIPTION,可 onSpeak 直通
viewModel::speak;而 beginTtsSpeak 的防抖只认 pending,播放中文件早就到手 ⇒ 不是
pending ⇒ 放行;整格被换成新的 pending 后缓存命中秒回,起播 effect 又因为「已经在放
同一条」跳过 toggleClip —— 唯一带停止语义的那条路径根本没走到。

改法:把「这一下是停还是念」抽成纯函数 ttsSpeakIntent(playback, messageId, clip),
判据落在那条唯一播放通道上(它是「有没有在响」的唯一真相),不再让三处各自解释
「播放中」是什么意思。ChatScreen 的 onSpeak 据它分派:Stop -> voicePlayer.stop(),
Speak -> viewModel.speak(message)。另需 clip.file != null:通道处于 Loading 时也算
active,但那一刻文件可能还没到手,判成 Stop 会停掉一次没起播的载入。

P2-1 「切走再切回补响旧朗读」—— 改成按「这一屏还在不在」判,而不是按时间:新增纯函数
ChatUiState.dropPendingTtsAutoPlay()(只对 autoPlay && file == null 的那一格置假),
由 ChatScreen 的 DisposableEffect.onDispose 调用。放弃「记发起时刻 + 超龄丢」是因为
时间不是这件事的判据 —— 合成一段长文本本来就可能花十几秒,按秒数丢会把「人就在看着
屏幕」的那一次也一起丢掉。配套回归点:applyTtsOutcome 的 autoPlay 改成从旧那一格
继承(原先硬写 false);否则作废之后结果晚到又会把闸门抬回真,等于没修。

P2-2 「TTS 缓存键缺 baseUrl」—— cacheName 的哈希输入改为
baseUrl.trimEnd('/') + "\n" + sessionId + "\n" + text。换一台服务器后,同一个
(sessionId, text) 在那边可能是另一个声音(人格映射 / 发音人 / 合成实现都是那台的),
命中上一台合成的 WAV 就是念错人。trimEnd('/') 归一,http://a/ 与 http://a 不分两份
缓存。类 KDoc 与 cacheName 的 KDoc 同步改写。

测试:TtsSpeakStateTest 11 -> 20(+9),SpeechClientTest 8 -> 10(+2)。
全量 testDebugUnitTest --rerun-tasks:138 suites / 1726 tests / 0 failures /
0 errors / 0 skipped(基线 138/1715,+11)。

仍未验:真机出声;请求在飞时没有「取消在飞请求」;(sessionId, text) 这份缓存对服务端
换发音人仍无失效手段(rev-w75 已同意这半边可接受)。
2026-10-26 23:52:23 +08:00
..