diff --git a/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md b/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md index 4755628..4b15bf2 100644 --- a/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md +++ b/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md @@ -6,8 +6,9 @@ > `README.md`(代码说明)、`部署说明.md`(运维)——这两份也在代码仓库。 > > 更新:2026-09-15。状态:**助手本体已在服务器上实现并跑通**(端到端测试 16/16); -> **Pipecat 传输层实验已做,传输层通、帧路由未通**(见 §10 与 -> `P0-Pipecat传输层实验报告.md`)——**A/B 架构方案待定**,这是当前卡点; +> **Pipecat 实时语音链路已闭环**(见 §10 与 +> `P0-Pipecat传输层实验报告.md`)——A 方案已确定:Pipecat 管传输/帧/RTVI, +> 自有引擎管会话语义;当前风险转为真实首音延迟与真人中文识别率; > 设备侧(N5095 外设桥、ESP32)尚未开始。 > 已确认:Ubuntu 24.04;N5095 / 16G 作为**客户端**;助手本体部署在 cadadmin-server(i9-14900K / 3090);麦克风、音箱、摄像头均未购;屏幕已于 09-14 到货并点亮(2.8 寸 ILI9341 触摸屏,非原定的 1.28 寸圆屏)。 > 核心目标:听得清、回应及时、可打断、表情明确、转头自然。 @@ -55,7 +56,7 @@ │ 会话引擎(身份 → 记忆 → LLM → TTS) │ │ ├─ LLM:DeepSeek deepseek-flash(云端,关思考) │ │ ├─ TTS:Qwen3-TTS 1.7B(RTX 3090,独立 HTTP 服务 :18040) │ -│ ├─ 传输:Pipecat SmallWebRTC :7860(实验,帧路由未通) │ +│ ├─ 传输:Pipecat SmallWebRTC :7860(实时链路已闭环) │ │ ├─ STT:Zipformer 中英宽带 int8(CPU 4 线程) │ │ ├─ 记忆:SQLite(按 owner_user_id 强制隔离) │ │ └─ 身份:手动切换 + 访客兜底 │ @@ -406,41 +407,34 @@ P5通过Home Assistant统一调用。按实际设备比较官方Xiaomi Home和Xi ### 下一步顺序(按风险从高到低) 评审建议的思路——**先把风险最大的实时语音链路闭环**——认可并采纳。 -但执行顺序要修正一处:**服务端目前没有任何客户端能连的入口**, -所以第一块不是客户端,是服务端的 WebRTC 入口。 - -> 核实过:代码里 `pipecat` 一次都没被 import 过(只在 `requirements.txt` 里), -> 引擎是独立 asyncio,由测试脚本用文件音频驱动。**没有进程在监听 WebRTC 连接。** +服务端 WebRTC 入口和客户端骨架现已完成。2026-09-16 通过 SSH 隧道打开 +Pipecat Playground 做了真人浏览器对话,麦克风 → STT → LLM → TTS → 浏览器播放 +正常。当前仍是 P1 调试形态:连接后持续上传并响应有效语音;正式的本地唤醒、 +待机不上传和 20 秒连续对话窗口留在 P2,本轮按决定暂不实现。 | # | 步骤 | 状态 | 能验证什么 / 不能验证什么 | |---|---|---|---| | 0 | **音频边界定稿** | ✅ 2026-09-15 完成(见 §5) | 决定了客户端接口形态 | -| 1 | **服务端 WebRTC 入口 + 客户端骨架** | 🔶 **传输层跑通,帧路由未通**(见 `P0-Pipecat传输层实验报告.md`) | 已验证:信令、ICE、data channel、双向媒体轨。**未验证:帧进不到自定义处理器,引擎从未运行** | -| 1b | **查清帧路由根因 → 定 A/B 方案** | **待办,当前卡点** | 根因决定 A 方案(Pipecat 只做传输)是否可行,还是必须转 B(按框架 service 模型重构) | +| 1 | **服务端 WebRTC 入口 + 客户端骨架** | ✅ **实时链路已闭环**(见 `P0-Pipecat传输层实验报告.md`) | 已验证:信令、ICE、data channel、帧路由、STT→LLM→TTS、文件与浏览器真人非静音回复 | +| 1b | **查清帧路由根因 → 定 A/B 方案** | ✅ **根因已定位,采用 A 方案** | NLTK 运行时下载导致 setup timeout;不是模型不兼容。网页 RTVI 保留 | | 2 | **N5095 客户端骨架** | 🔶 雏形已有(`client/test_client.py`) | 非 trickle ICE、data channel 心跳、文件音频实时发送、回复录音都已实现。**不做 AEC/播客参考/打断**——那些要真实声卡 | -| 3 | **接入真实麦克风与音箱** | 阻塞:硬件未购 | 唯一能验证 AEC、打断、唤醒的一步,也是 P2 的全部内容 | +| 3 | **接入真实麦克风与音箱** | 阻塞:硬件未购;网页仅验证普通麦克风对话 | 唯一能完整验证阵列 AEC、打断、本地唤醒的一步,也是 P2 的全部内容 | | 4 | **STT 中文识别正确率** | 阻塞:需录音 | 见下"最大功能风险" | | 5 | ESP32 表情串口 | 硬件已到货,可独立推进 | 串口协议、表情渲染 | | 6 | 摄像头、多人跟踪、电机 | 阻塞:硬件未购 | — | ### 第 1 步的实验结论(2026-09-15) -**传输层可用,但"把整条会话逻辑塞进一个 FrameProcessor"不是开箱即用的。** +**实时链路已闭环,A 方案成立。** 原 `process_frame` 为 0 的根因是 Pipecat +setup 阶段预热 NLTK `punkt_tab`,首次下载阻塞后触发 20 秒 timeout;worker 尚未 +发送自动 StartFrame,并非 Pipeline 接线或自定义处理器模型不兼容。 -自定义处理器被创建、`setup()` 被调用,但 **`process_frame` 调用次数为 0**—— -连自己注入的 StartFrame 都没进去。现象是"连接全对、音轨协商成功、 -但助手永远不说话,且日志里一条错误都没有"。 +修复后自动 StartFrame、250+ 个输入音频帧均正常经过 `EngineProcessor`,完整 +文件音频 → STT → DeepSeek → Qwen3-TTS → WebRTC 返回非静音语音。保留 RTVI +和 `/client/` 网页入口,同时继续支持自写 aiortc/N5095 客户端。 -**这对 A/B 决策的影响**:实验前判断"Pipecat 的价值收敛到传输层", -现在要修正——**传输层与自定义逻辑之间的接缝比预想的深**。 -B 方案恰恰绕开这个接缝(用框架原生 service,不由一个处理器吞掉全部)。 - -**但根因未定位**,两种可能必须区分: -- 若是接线错误(管线用法与 Pipecat 1.10 的实际约定不符)→ 接缝浅,A 仍可行 -- 若是模型不兼容(该版本不接受这种用法)→ 接缝深,应转 B - -实验同时挖出 **8 个 Pipecat 集成坑**(客户端需发 ping、offer 无媒体则不收集 -ICE 候选、`run()` 要 WorkerParams、后台异常被静默吞掉等), +实验累计挖出 **13 个集成坑**(客户端需发 ping、offer 无媒体则不收集 +ICE 候选、Playground 需要 `/start`、`run()` 要 WorkerParams、后台异常被静默吞掉等), 每个都表现为"什么都没发生",详见实验报告。 **第 2 步的边界要说清楚**:用合成音频能验证传输层,**验不了 AEC 和远场识别**—— diff --git a/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md b/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md index ea855ba..e8c176b 100644 --- a/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md +++ b/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md @@ -1,6 +1,6 @@ # P0 实验报告 · Pipecat 传输层 -日期:2026-09-15。状态:**传输层跑通,帧路由未跑通——实验目标部分达成。** +日期:2026-09-15。状态:**实时语音链路已闭环;A 方案验证通过。** 代码:`~/workspace/ai-companion/services/bot_server.py`(服务端)、 `client/test_client.py`(自写 aiortc 客户端) @@ -26,27 +26,31 @@ | ICE 连通 | ✅ 走 172.16.1.x 直连 | | data channel + 心跳 | ✅ | | 双向媒体轨 | ✅ 客户端音轨送达服务端,服务端音轨客户端收到 | -| 管线启动、处理器 setup() | ✅ | -| **帧路由到自定义处理器** | ❌ **`process_frame` 调用次数 = 0** | -| 端到端出声 | ❌ 因上一行,引擎从未运行 | +| 管线启动、处理器 setup() | ✅,只产生一个自动 StartFrame | +| **帧路由到自定义处理器** | ✅ StartFrame 与 250+ 个音频帧实测到达 | +| 端到端出声 | ✅ 文件音频 → STT → DeepSeek → Qwen3-TTS → 非静音回复 | +| 浏览器真人对话 | ✅ SSH 隧道打开 Playground,麦克风输入与语音回复正常 | +| 单次真实链路延迟 | 源音频结束→客户端首个非静音约 5.8s;仍需优化 | -**即:传输层可用,但"把整条会话逻辑塞进一个 FrameProcessor"不是开箱即用的。** +**即:Pipecat 负责传输/帧/RTVI、自有引擎负责会话语义的 A 方案成立。** -## 三、没跑通的部分(关键) +## 三、原问题根因与修复 -自定义 `EngineProcessor` 被创建了、`setup()` 也被调用了,但**一帧都没进来**—— -连自己用 `task.queue_frames([StartFrame()])` 注入的 StartFrame 都没有。 +原先看到 `EngineProcessor.setup()` 被调用、`process_frame` 却为 0,于是误判为 +Pipeline 接线或模型不兼容。服务器完整日志揭示了真实顺序:Pipecat 在 setup 阶段 +预热 NLTK 分句器,首次运行尝试下载 `punkt_tab`,20 秒后 setup timeout。worker +尚未走到自动发送 `StartFrame` 的阶段,因此所有业务帧都还在等待。 -现象是:**连接全对、音轨协商成功、ICE connected,但助手永远不说话, -而且日志里连一条错误都没有。** +修复后把 `punkt_tab` 预装到权限 700 的 +`~/.local/share/ai-companion/nltk_data`,启动时 fail-fast 检查;同时改用 +`PipelineWorker(params=PIPELINE_PARAMS)`,删除客户端连接事件中手工发送的第二个 +`StartFrame`。复测显示自动 StartFrame 正常抵达管线末端,音频持续进入处理器。 -这与 A 方案的核心假设直接相关:如果帧送不进自定义处理器, -"Pipecat 只做传输、逻辑归引擎"这个切分方式就不成立。 +之后仍不出声是第二个独立问题:文件客户端播放完 WAV 后没有继续推进 PTS,服务端 +收不到 VAD 所需的 700ms 尾静音。客户端现在固定发送 1.2s 尾静音且持续推进时钟。 +接收端也改为只有检测到非静音音频才算成功,不再把 WebRTC 空闲静音当作回复。 -**根因未定位。** 不能确定是这个版本 Pipecat 的管线用法与我们写的不符, -还是模型本身不接受这种用法。这两者的区别很重要——见 §五。 - -## 四、挖出的 8 个集成坑 +## 四、挖出的 13 个集成坑 每个都会表现为"什么都没发生",极难排查。无论最终选 A 还是 B 都会踩。 @@ -56,10 +60,15 @@ | 2 | 客户端必须建 data channel 并定期发 `"ping"` | 不建则服务端**一帧音频都不推**,日志只有一句 "Data channel not established within 10s" | 建 channel + 1s 心跳 | | 3 | offer 里没有媒体时 aiortc **完全不收集 ICE 候选** | 信令成功、SDP 交换正常,但 ICE 永远停在 `new` | 即使只测连通也要加一条(静音)音轨 | | 4 | ICE 收集完成前就发 offer | SDP 里 0 个候选地址 | 等 `iceGatheringState == "complete"` | -| 5 | `PipelineTask.run()` 要 `WorkerParams(task_manager=...)`,**不是** `PipelineParams` | 不传报 "missing 1 required positional argument";传错报 "no attribute 'task_manager'" | 两个参数分别给构造函数和 run() | +| 5 | `PipelineWorker.run()` 要 `WorkerParams(task_manager=...)`,**不是** `PipelineParams` | 不传报 "missing 1 required positional argument";传错报 "no attribute 'task_manager'" | 两个参数分别给构造函数和 run() | | 6 | `TaskManager()` 内部调 `asyncio.get_running_loop()` | 模块级创建报 "no running event loop" | 必须在异步上下文里建 | | 7 | **后台任务里的异常被静默吞掉** | 日志里查不到任何线索,表现为"连接正常但没反应" | 自己 try/except + log.exception | | 8 | 信令回调里 `await task.run()` | 阻塞整个会话生命周期,管线初始化时序走歪 | `asyncio.create_task(task.run(...))` | +| 9 | NLTK `punkt_tab` 在首个 worker 内临时下载 | setup 20 秒超时,看起来像帧路由坏了 | 部署时预装到私有目录,启动前检查 | +| 10 | `PipelineWorker` 本身会自动发送 StartFrame | 手工再发会看到两个启动帧 | 删除连接回调里的手工 StartFrame | +| 11 | 文件结束后静音帧 PTS 不增长 | VAD 永远等不到说完 | 源位置与媒体时钟分离,额外发送 1.2s 静音 | +| 12 | WebRTC 输出轨空闲时也有静音 | “收到 34 秒音频/首音 206ms”的假成功 | 按非静音 RMS 判断业务回复 | +| 13 | prebuilt Playground 连接前先请求 `/start` | 页面正常但 Connect 在 authenticating 阶段报 Not Found | 增加 `/start`,返回 `/api/offer` 的 `webrtcRequestParams` | 第 5、6、7 条各花了好几轮才发现——因为异常在后台任务里,日志什么都不显示。 @@ -68,44 +77,38 @@ 实验前的判断是"Pipecat 的独特价值收敛到传输层"(因为方案 §5 的音频边界决定 把 AEC 和播放放在客户端,绕开了 Pipecat 的打断机制)。 -**现在要修正**: +根因已经证明既不是 Pipeline 接线,也不是自定义处理器模型不兼容。因此确定采用 +**A 方案**:保留已验证的 `ConversationEngine`,由薄 `EngineProcessor` 适配 +Pipecat 的输入输出帧。网页客户端需要的 RTVI 继续开启;N5095 精简客户端可以忽略 +暂不认识的 RTVI data-channel 消息。 -- 传输层确实能用——前半部分的结论成立 -- 但**传输层与自定义会话逻辑之间的接缝,比预想的深** -- B 方案恰恰绕开这个接缝:它不把逻辑塞进一个处理器, - 而是用框架原生的 STT/LLM/TTS service,由框架自己编排 - -所以这次实验**没有支持 A 方案**,反而间接给了 B 方案更多理由。 - -**但前提是根因要查清**: - -- 如果是我接线错(管线用法和 Pipecat 1.10 的实际约定不符)→ 接缝是浅的,A 仍然可行 -- 如果是模型不兼容(这个版本不接受"单一处理器吞整条管线")→ 接缝是深的,应该转 B +不做 B 方案的全量重构。后续只在确有收益时渐进引入 Pipecat 的 VAD、turn processor、 +metrics 与 interruption,不重写已有的记忆、身份、LLM/TTS 适配。 ## 六、下一步 -**建议先查清帧路由的根因**,再决定 A 还是 B。候选方向: - -1. 对照 Pipecat 官方 smallwebrtc 示例的管线接法(我们的接法可能有细节差异) -2. 检查 `Pipeline([...])` 在 1.10 里的连接语义(是否有自动 link) -3. 检查 `queue_frames` 的注入点与处理器位置的匹配关系 -4. 在传输层的 input/output 处理器上加日志,看帧到底停在哪一环 - -若排查成本过高,**建议直接转 B**——因为 B 用的是框架原生 service, -不存在"帧送不进自定义处理器"这个问题。 +1. 优化真实首音延迟:分别记录 VAD、STT、LLM 首 token、TTS 首段和客户端播放时间 +2. 继续用 prebuilt 网页客户端做真人语音、浏览器 AEC 与主观试听(基础对话已通过) +3. Tailscale tailnet 开启 Serve 后,用 HTTPS 暴露 `/client/` +4. 音频硬件到货后实现 N5095 声卡、AEC、播放参考与打断 +5. 录本人普通话语料,对 Zipformer / SenseVoice / faster-whisper 做 CER 对比 ## 七、当前代码状态 **服务端** `services/bot_server.py`: - 已实现:SmallWebRTC 信令端点 `/api/offer`、prebuilt 网页客户端挂载 `/client/`、 - `EngineProcessor`(自写能量 VAD + 引擎调用) -- **仍带着诊断日志**(`[诊断]` 前缀),用于下一轮排查。定位后应清理 + Playground 启动端点 `/start`、`EngineProcessor`(自写能量 VAD + 引擎调用) +- 已改用 `PipelineWorker`,保留 RTVI;启动/超时/错误有显式日志 +- 音频缓冲加入 1 秒预卷、60 秒上限和短噪声丢弃,连接结束释放会话资源 - VAD 是**极简能量判定**,不是最终方案(方案 §5 的说完判定还没选型) +- 当前没有唤醒门控:网页连接后每段有效语音都会触发回复。正式唤醒仍在 P2 + 的 N5095 本地实现,待机时不上传音频;本次按用户决定暂不处理 **客户端** `client/test_client.py`: -- 已实现:非 trickle ICE、data channel + 心跳、文件音频按实时速率发送、回复录音 +- 已实现:非 trickle ICE、data channel + 心跳、文件音频实时发送、1.2s 尾静音、回复录音 +- 只有检测到非静音回复才成功;按 WebRTC 实际 48kHz 保存 WAV - **不做 AEC、不做播放参考、不做打断**——那些要真实声卡和硬件 - 它是 N5095 客户端的雏形,不是产品代码 @@ -123,7 +126,9 @@ python client/test_client.py \ --wav p0/stt_tts_bench/wavs/stt_1.wav --out /tmp/reply.wav ``` -预期(当前实际):连接成功、收到 34 秒音频轨,**但全是静音**。 +当前实测:连接成功,收到非静音语音回复;源音频结束到首个非静音约 5.8 秒。 +浏览器通过 SSH 隧道访问 `http://localhost:7860/client/` 也已完成真人对话; +2026-09-16 一轮服务端指标约 3.0 秒。 ## 九、顺带发现的环境问题