Files
note_sync/有趣的项目/AI桌面陪伴助手/P0-Pipecat传输层实验报告.md
T
ylhan 2ef6fe215c 新增 Pipecat 传输层实验报告;方案 §10 记录当前卡点
背景:方案文档写"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
2026-09-15 17:55:53 +08:00

137 lines
6.8 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` 的残留引用。