From 95ee66e836fce8dc8cd2143420406f0295967bff Mon Sep 17 00:00:00 2001 From: ssurui Date: Tue, 6 Oct 2026 10:35:03 +0800 Subject: [PATCH] =?UTF-8?q?docs:=20=E8=AE=B0=E5=BD=95=E9=9F=B3=E9=A2=91?= =?UTF-8?q?=E9=80=9A=E8=B7=AF=E5=8D=A1=E6=AD=BB=E5=AF=BC=E8=87=B4=E8=AF=AD?= =?UTF-8?q?=E9=9F=B3=E8=BE=93=E5=85=A5=E8=BE=93=E5=87=BA=E5=A4=B1=E6=95=88?= =?UTF-8?q?=E7=9A=84=E6=A0=B9=E5=9B=A0=E4=B8=8E=20SIGSTOP=20=E5=A4=8D?= =?UTF-8?q?=E7=8E=B0=E6=89=8B=E6=B3=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 补充长时间运行后 TTS/ASR 一起失效的真实根因(播放阶段缺超时兜底 导致 aplay 卡死、isSpeaking 永久卡住、僵尸进程拖垮 PulseAudio), 以及用 kill -STOP/-CONT 冻结下游服务来可控复现"子进程偶发挂死" 类 bug 的通用排查手法。 Co-Authored-By: Claude Sonnet 5 Claude-Session: https://claude.ai/code/session_01QeoAy5KmkHuKWHAktpEsaN --- CLAUDE.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index 2cc0c6e..32b4b66 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -207,6 +207,16 @@ const vectors = await Promise.all(texts.map((t, i) => fetchOne(t, i))) **调试这类"实际发声内容对不对"的问题时的教训**:想在渲染进程 monkey-patch `window.electronAPI.xxx` 来拦截调用做验证是无效的——`contextBridge.exposeInMainWorld` 暴露的对象默认被冻结,赋值会静默失败、不报错,很容易误判成"这条代码路径没走到"。真正能验证的办法是去主进程对应的 `ipcMain.handle(...)` 里临时加 `console.log`(验证完记得删),或者用 CDP 的 `Input.dispatchMouseEvent` 模拟真实点击、`Runtime.evaluate` 读 DOM/计算样式——这两种都是从"实际发生的效果"验证,不依赖能否篡改渲染进程里的 API 对象。 +### 长时间运行后语音输入输出一起失效(systemTts.ts) + +真机反馈:pad 跑了一两周之后语音输入输出全部失效。现场诊断:pulseaudio 占 49% CPU,系统里积压了 70 个从几天前就存在的 `aplay -t raw` 残留进程。 + +**根因跟 `WAKE_IDLE_THRESHOLD_MS` 那条注释里提到的"PulseAudio 大约 5 秒空闲就挂起音频设备"是同一件事**:`speakWithVits`/`speakWithPiperStreaming` 每播一个分句就 spawn 一个 aplay,写完数据后调用 `stdin.end()`,然后完全依赖 aplay 自己触发 `'exit'` 事件才清理 `isSpeaking` 状态——没有任何超时兜底(合成侧 `vitsWorker` 倒是本来就有 60 秒超时)。长时间运行后一旦某次"唤醒音频通路"没成功,aplay 会卡在往挂死的 PulseAudio 写数据,`'exit'` 永远不触发,`isSpeaking` 永久卡 `true`,`playNext()` 不再处理队列,TTS 直接哑掉。**ASR 走的是渲染进程 `getUserMedia`/`AudioContext`,和 TTS 共用同一个被拖垮的 PulseAudio 实例**,所以语音输入也一起失效。更麻烦的是 `systemTtsStop()` 原来只发 `SIGTERM`,卡在系统调用里的 aplay 根本杀不掉——每次打断播报(每次提问前都会调一次)都可能攒下一个杀不死的僵尸进程,日积月累到 70 个,把 PulseAudio 彻底拖垮。 + +**修复**:播放阶段加 `PLAYER_EXIT_TIMEOUT_MS`(8 秒)看门狗,`stdin.end()` 后超时未触发 `'exit'` 就判定通路卡死,`SIGKILL` 强杀并 `done(error)` 收尾;`systemTtsStop()` 加 `killWithEscalation()`,先 `SIGTERM`,1.5 秒宽限期后还没退出就 `SIGKILL`。 + +**验证方法**:真机上拿 `kill -STOP ` 故意冻结 PulseAudio,模拟"音频通路卡死"的真实场景,再用 CDP 触发一次播报,确认看门狗在约 8 秒后正确强杀卡住的 aplay、队列自动继续、最终无残留进程,`kill -CONT` 解冻收尾。**这个冻结/解冻手法可以复用**:任何"子进程偶发挂死"的 bug,不必真的等它在真机自然复现,直接 `SIGSTOP` 它依赖的下游服务就能可控地造出卡死场景。 + ### 系统提示词:查不到内容时的答复 提示词要求"说明手册中暂未查到,并给一条可操作的建议",而不是硬邦邦一句"我不知道"。