From 2ef6fe215cb0ac9c3ae4c73dac9b932ad45fab95 Mon Sep 17 00:00:00 2001 From: ylhan Date: Tue, 15 Sep 2026 17:55:53 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Pipecat=20=E4=BC=A0?= =?UTF-8?q?=E8=BE=93=E5=B1=82=E5=AE=9E=E9=AA=8C=E6=8A=A5=E5=91=8A=EF=BC=9B?= =?UTF-8?q?=E6=96=B9=E6=A1=88=20=C2=A710=20=E8=AE=B0=E5=BD=95=E5=BD=93?= =?UTF-8?q?=E5=89=8D=E5=8D=A1=E7=82=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 背景:方案文档写"Pipecat 作为唯一会话管线框架",但核实后发现 代码里它只在 requirements.txt 里(零引用)。于是做实验验证架构问题: Pipecat 该用多深——只做传输(A),还是按框架 service 模型重构(B)。 新增 P0-Pipecat传输层实验报告.md - 传输层跑通:信令(47–280ms)、ICE 连通、data channel、双向媒体轨 - 帧路由未通:自定义处理器的 process_frame 调用次数为 0, 连自己注入的 StartFrame 都没进去;引擎从未运行 - 现象:连接全对、音轨协商成功、助手永远不说话,日志里一条错误都没有 - 记下 8 个集成坑,每个都表现为"什么都没发生" - 结论:实验未能支持 A 方案,反而间接给了 B 更多理由; 但根因未定位,两种可能必须区分(接线错误 vs 模型不兼容) 方案 §10 更新 - 第 1 步改为"服务端 WebRTC 入口 + 客户端骨架",状态标注为 "传输层跑通、帧路由未通" - 新增 1b 步:查清帧路由根因 → 定 A/B 方案,标为当前卡点 - header 加注实验状态与 A/B 待定 - 架构图补传输层,标注"实验,帧路由未通" 顺带修正 - 局域网于当日重新编址(192.168.0.x → 172.16.1.x), 架构图里的服务器地址更新 - 报告数量 4 → 5 --- .../AI桌面陪伴助手/AI桌面陪伴助手方案.md | 37 ++++- .../P0-Pipecat传输层实验报告.md | 136 ++++++++++++++++++ 2 files changed, 167 insertions(+), 6 deletions(-) create mode 100644 有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md diff --git a/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md b/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md index e47d53c..4755628 100644 --- a/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md +++ b/有趣的项目/AI桌面陪伴助手/AI桌面陪伴助手方案.md @@ -5,7 +5,10 @@ > (独立 git 仓库),包括:助手本体、TTS 服务、P0 基准脚本、ESP32 固件、 > `README.md`(代码说明)、`部署说明.md`(运维)——这两份也在代码仓库。 > -> 更新:2026-09-15。状态:**助手本体已在服务器上实现并跑通**(端到端测试 16/16);设备侧(N5095 外设桥、ESP32)尚未开始。 +> 更新:2026-09-15。状态:**助手本体已在服务器上实现并跑通**(端到端测试 16/16); +> **Pipecat 传输层实验已做,传输层通、帧路由未通**(见 §10 与 +> `P0-Pipecat传输层实验报告.md`)——**A/B 架构方案待定**,这是当前卡点; +> 设备侧(N5095 外设桥、ESP32)尚未开始。 > 已确认:Ubuntu 24.04;N5095 / 16G 作为**客户端**;助手本体部署在 cadadmin-server(i9-14900K / 3090);麦克风、音箱、摄像头均未购;屏幕已于 09-14 到货并点亮(2.8 寸 ILI9341 触摸屏,非原定的 1.28 寸圆屏)。 > 核心目标:听得清、回应及时、可打断、表情明确、转头自然。 > 最新外形要求:屏幕不用太大;摄像头水平可用旋转总行程至少180°,按正前方左右各90°理解,不是镜头视场角180°。 @@ -22,7 +25,7 @@ > > 变更后 N5095 只做 USB 外设接入与网络桥接,**不运行 STT/TTS/LLM 等重型推理** > (轻量推理仍在本地,见 §1)。 -> 详细部署现状见代码仓库里的 `部署说明.md`,实测数据见本目录的四份 P0 报告。 +> 详细部署现状见代码仓库里的 `部署说明.md`,实测数据见本目录的五份 P0 报告。 ## 1. 范围与待讨论事项 @@ -48,15 +51,16 @@ ## 2. 总体架构 ```text -┌─ cadadmin-server(192.168.0.20)—— 助手本体 ────────────────┐ +┌─ cadadmin-server(172.16.1.101)—— 助手本体 ────────────────┐ │ 会话引擎(身份 → 记忆 → LLM → TTS) │ │ ├─ LLM:DeepSeek deepseek-flash(云端,关思考) │ │ ├─ TTS:Qwen3-TTS 1.7B(RTX 3090,独立 HTTP 服务 :18040) │ +│ ├─ 传输:Pipecat SmallWebRTC :7860(实验,帧路由未通) │ │ ├─ STT:Zipformer 中英宽带 int8(CPU 4 线程) │ │ ├─ 记忆:SQLite(按 owner_user_id 强制隔离) │ │ └─ 身份:手动切换 + 访客兜底 │ └────────────────────────────────────────────────────────────┘ - ▲ WebRTC(计划) ▲ 手机/浏览器客户端 + ▲ WebRTC(实验中) ▲ 手机/浏览器客户端 │ 局域网 1–2ms │ ┌─ N5095 / 16G —— 客户端 / 外设桥 ───────────────────────────┐ │ USB 麦克风阵列(固定底座)──处理后音频、DoA──┐ │ @@ -411,13 +415,34 @@ P5通过Home Assistant统一调用。按实际设备比较官方Xiaomi Home和Xi | # | 步骤 | 状态 | 能验证什么 / 不能验证什么 | |---|---|---|---| | 0 | **音频边界定稿** | ✅ 2026-09-15 完成(见 §5) | 决定了客户端接口形态 | -| 1 | **服务端 WebRTC 入口**(Pipecat SmallWebRTC) | 未开始 | 让客户端有东西可连 | -| 2 | **N5095 客户端骨架** | 未开始 | 用**合成音频**即可跑通传输层:信令稳定性、帧格式、采样率边界、打断信号传递、**真实传输下的首音延迟** | +| 1 | **服务端 WebRTC 入口 + 客户端骨架** | 🔶 **传输层跑通,帧路由未通**(见 `P0-Pipecat传输层实验报告.md`) | 已验证:信令、ICE、data channel、双向媒体轨。**未验证:帧进不到自定义处理器,引擎从未运行** | +| 1b | **查清帧路由根因 → 定 A/B 方案** | **待办,当前卡点** | 根因决定 A 方案(Pipecat 只做传输)是否可行,还是必须转 B(按框架 service 模型重构) | +| 2 | **N5095 客户端骨架** | 🔶 雏形已有(`client/test_client.py`) | 非 trickle ICE、data channel 心跳、文件音频实时发送、回复录音都已实现。**不做 AEC/播客参考/打断**——那些要真实声卡 | | 3 | **接入真实麦克风与音箱** | 阻塞:硬件未购 | 唯一能验证 AEC、打断、唤醒的一步,也是 P2 的全部内容 | | 4 | **STT 中文识别正确率** | 阻塞:需录音 | 见下"最大功能风险" | | 5 | ESP32 表情串口 | 硬件已到货,可独立推进 | 串口协议、表情渲染 | | 6 | 摄像头、多人跟踪、电机 | 阻塞:硬件未购 | — | +### 第 1 步的实验结论(2026-09-15) + +**传输层可用,但"把整条会话逻辑塞进一个 FrameProcessor"不是开箱即用的。** + +自定义处理器被创建、`setup()` 被调用,但 **`process_frame` 调用次数为 0**—— +连自己注入的 StartFrame 都没进去。现象是"连接全对、音轨协商成功、 +但助手永远不说话,且日志里一条错误都没有"。 + +**这对 A/B 决策的影响**:实验前判断"Pipecat 的价值收敛到传输层", +现在要修正——**传输层与自定义逻辑之间的接缝比预想的深**。 +B 方案恰恰绕开这个接缝(用框架原生 service,不由一个处理器吞掉全部)。 + +**但根因未定位**,两种可能必须区分: +- 若是接线错误(管线用法与 Pipecat 1.10 的实际约定不符)→ 接缝浅,A 仍可行 +- 若是模型不兼容(该版本不接受这种用法)→ 接缝深,应转 B + +实验同时挖出 **8 个 Pipecat 集成坑**(客户端需发 ping、offer 无媒体则不收集 +ICE 候选、`run()` 要 WorkerParams、后台异常被静默吞掉等), +每个都表现为"什么都没发生",详见实验报告。 + **第 2 步的边界要说清楚**:用合成音频能验证传输层,**验不了 AEC 和远场识别**—— 那些必须等真实麦克风和音箱。但传输层能提前跑通,硬件一到就能直接接, 不用同时调两件事。 diff --git a/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md b/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md new file mode 100644 index 0000000..ea855ba --- /dev/null +++ b/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md @@ -0,0 +1,136 @@ +# P0 实验报告 · Pipecat 传输层 + +日期:2026-09-15。状态:**传输层跑通,帧路由未跑通——实验目标部分达成。** + +代码:`~/workspace/ai-companion/services/bot_server.py`(服务端)、 +`client/test_client.py`(自写 aiortc 客户端) + +## 一、为什么做这个实验 + +方案文档写"Pipecat 作为唯一会话管线框架",但核实后发现**它一行都没被用上** +(只在 `requirements.txt` 里声明,代码里零引用)。助手引擎是独立 asyncio, +由测试脚本用文件音频驱动。 + +于是有个待定的架构问题:**Pipecat 该用多深?** + +- **A 方案**:Pipecat 只做传输与帧模型,会话语义归自有引擎(一个 FrameProcessor 吞掉整条管线) +- **B 方案**:按 Pipecat 的 service 模型重构,STT/LLM/TTS/记忆都做成框架原生 service + +实验目的:验证 A 方案的核心假设能否成立,并拿到真实传输下的延迟数字。 + +## 二、结论摘要 + +| 部分 | 结果 | +|---|---| +| WebRTC 信令(offer/answer) | ✅ 往返 47–280ms | +| ICE 连通 | ✅ 走 172.16.1.x 直连 | +| data channel + 心跳 | ✅ | +| 双向媒体轨 | ✅ 客户端音轨送达服务端,服务端音轨客户端收到 | +| 管线启动、处理器 setup() | ✅ | +| **帧路由到自定义处理器** | ❌ **`process_frame` 调用次数 = 0** | +| 端到端出声 | ❌ 因上一行,引擎从未运行 | + +**即:传输层可用,但"把整条会话逻辑塞进一个 FrameProcessor"不是开箱即用的。** + +## 三、没跑通的部分(关键) + +自定义 `EngineProcessor` 被创建了、`setup()` 也被调用了,但**一帧都没进来**—— +连自己用 `task.queue_frames([StartFrame()])` 注入的 StartFrame 都没有。 + +现象是:**连接全对、音轨协商成功、ICE connected,但助手永远不说话, +而且日志里连一条错误都没有。** + +这与 A 方案的核心假设直接相关:如果帧送不进自定义处理器, +"Pipecat 只做传输、逻辑归引擎"这个切分方式就不成立。 + +**根因未定位。** 不能确定是这个版本 Pipecat 的管线用法与我们写的不符, +还是模型本身不接受这种用法。这两者的区别很重要——见 §五。 + +## 四、挖出的 8 个集成坑 + +每个都会表现为"什么都没发生",极难排查。无论最终选 A 还是 B 都会踩。 + +| # | 坑 | 现象 | 怎么办 | +|---|---|---|---| +| 1 | Pipecat 客户端 SDK 不覆盖 N5095 形态 | 浏览器/移动端/C++/ESP32 都有,没有"Linux x86 盒子接 USB 阵列" | 自己写 aiortc 客户端(已有雏形) | +| 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() | +| 6 | `TaskManager()` 内部调 `asyncio.get_running_loop()` | 模块级创建报 "no running event loop" | 必须在异步上下文里建 | +| 7 | **后台任务里的异常被静默吞掉** | 日志里查不到任何线索,表现为"连接正常但没反应" | 自己 try/except + log.exception | +| 8 | 信令回调里 `await task.run()` | 阻塞整个会话生命周期,管线初始化时序走歪 | `asyncio.create_task(task.run(...))` | + +第 5、6、7 条各花了好几轮才发现——因为异常在后台任务里,日志什么都不显示。 + +## 五、对 A/B 决策的影响 + +实验前的判断是"Pipecat 的独特价值收敛到传输层"(因为方案 §5 的音频边界决定 +把 AEC 和播放放在客户端,绕开了 Pipecat 的打断机制)。 + +**现在要修正**: + +- 传输层确实能用——前半部分的结论成立 +- 但**传输层与自定义会话逻辑之间的接缝,比预想的深** +- B 方案恰恰绕开这个接缝:它不把逻辑塞进一个处理器, + 而是用框架原生的 STT/LLM/TTS service,由框架自己编排 + +所以这次实验**没有支持 A 方案**,反而间接给了 B 方案更多理由。 + +**但前提是根因要查清**: + +- 如果是我接线错(管线用法和 Pipecat 1.10 的实际约定不符)→ 接缝是浅的,A 仍然可行 +- 如果是模型不兼容(这个版本不接受"单一处理器吞整条管线")→ 接缝是深的,应该转 B + +## 六、下一步 + +**建议先查清帧路由的根因**,再决定 A 还是 B。候选方向: + +1. 对照 Pipecat 官方 smallwebrtc 示例的管线接法(我们的接法可能有细节差异) +2. 检查 `Pipeline([...])` 在 1.10 里的连接语义(是否有自动 link) +3. 检查 `queue_frames` 的注入点与处理器位置的匹配关系 +4. 在传输层的 input/output 处理器上加日志,看帧到底停在哪一环 + +若排查成本过高,**建议直接转 B**——因为 B 用的是框架原生 service, +不存在"帧送不进自定义处理器"这个问题。 + +## 七、当前代码状态 + +**服务端** `services/bot_server.py`: + +- 已实现:SmallWebRTC 信令端点 `/api/offer`、prebuilt 网页客户端挂载 `/client/`、 + `EngineProcessor`(自写能量 VAD + 引擎调用) +- **仍带着诊断日志**(`[诊断]` 前缀),用于下一轮排查。定位后应清理 +- VAD 是**极简能量判定**,不是最终方案(方案 §5 的说完判定还没选型) + +**客户端** `client/test_client.py`: + +- 已实现:非 trickle ICE、data channel + 心跳、文件音频按实时速率发送、回复录音 +- **不做 AEC、不做播放参考、不做打断**——那些要真实声卡和硬件 +- 它是 N5095 客户端的雏形,不是产品代码 + +## 八、复现 + +```bash +# 服务端(服务器上) +cd ~/ai-companion +.venv/bin/python services/bot_server.py --host 0.0.0.0 --port 7860 + +# 客户端(N5095 上) +cd ~/workspace/ai-companion +python client/test_client.py \ + --server http://cadadmin-server.taild0f91d.ts.net:7860 \ + --wav p0/stt_tts_bench/wavs/stt_1.wav --out /tmp/reply.wav +``` + +预期(当前实际):连接成功、收到 34 秒音频轨,**但全是静音**。 + +## 九、顺带发现的环境问题 + +**局域网被重新编址了**:两台机器从 `192.168.0.x` 变成 `172.16.1.x` +(N5095 现为 `172.16.1.99`,服务器 `172.16.1.101`)。 + +已处理:客户端默认地址改用 tailscale 域名 +(`cadadmin-server.taild0f91d.ts.net`),抗重新编址。 + +**未处理**:方案文档与 N5095 配置里仍有 `192.168.0.20` 的残留引用。