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

6.8 KiB
Raw Blame History

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 客户端的雏形,不是产品代码

八、复现

# 服务端(服务器上)
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 的残留引用。