ylhan 74a30b11d7 Pipecat 传输层实验:传输层通,帧路由未通
新增实验代码
- services/bot_server.py:Pipecat SmallWebRTC 服务端,
  把 ConversationEngine 包成一个 FrameProcessor(检验 A 方案的核心假设)
- client/test_client.py:自写 aiortc 客户端——Pipecat 的客户端 SDK
  覆盖浏览器/移动端/C++/ESP32,没有适配"Linux x86 盒子接 USB 阵列"这种形态

实验结果
- ✅ 传输层跑通:信令(47–280ms 往返)、ICE 连通、data channel、
  双向媒体轨(客户端音轨送达服务端,服务端音轨客户端收到)
- ❌ 帧路由未通:自定义处理器的 process_frame 调用次数为 0,
  连自己注入的 StartFrame 都没进去,引擎从未运行
- 现象:连接全对、音轨协商成功、助手永远不说话,日志里一条错误都没有

对架构决策的影响
A 方案(Pipecat 只做传输、逻辑归自有引擎)的核心假设不是开箱即用的。
B 方案按框架原生 service 重构,绕开了这个接缝。
根因未定位:接线错误(接缝浅,A 可行)vs 模型不兼容(接缝深,应转 B)。

顺带挖出 8 个 Pipecat 集成坑,每个都表现为"什么都没发生",已记入 README:
客户端需发 data channel ping、offer 无媒体则不收集 ICE 候选、
run() 要 WorkerParams、TaskManager 需在事件循环内建、
后台任务异常被静默吞掉等。

其他
- bot_server.py 保留 [诊断] 前缀日志,供下一轮排查,定位后应清理
- 客户端默认地址改用 tailscale 域名(局域网已于当日重新编址,
  从 192.168.0.x 变为 172.16.1.x)
- 新增依赖 aiortc(含 av 17.x,注意 18.x 与 aiortc 1.15 不兼容)
2026-09-15 17:54:34 +08:00

AI 桌面陪伴助手 · 代码说明

架构一句话:服务器是助手,本地是客户端。 助手本体(会话引擎、记忆、身份、STT/TTS 调度)跑在 cadadmin-server; N5095 只做 USB 外设接入和网络桥接。理由与实测见 P0-语音基准报告.md。

运行方式与配置见 部署说明.md;总体方案见 AI桌面陪伴助手方案.md。

目录结构

app/                    助手本体(部署到服务器)
├── config.py           配置。全部可用环境变量覆盖——部署时不改代码
├── prompts.py          提示词构造。单条 system 每轮重建
├── engine.py           会话引擎:身份 → 记忆 → LLM → TTS 的核心回路
├── llm/                LLM 抽象与后端
│   ├── base.py         接口 + SSE 解析 + 消息归一化
│   ├── local_vllm.py   本机 vLLM(Qwen3.8-27B,已停用)
│   ├── deepseek.py     DeepSeek 云 API(当前主用)
│   └── __init__.py     工厂 + 降级路由
├── memory/             记忆
│   ├── base.py         接口。**按"可能远程"设计**——全 async、可超时、可失败
│   └── sqlite_store.py SQLite 实现
├── identity/           身份
│   ├── base.py         接口。人脸只提供候选,**不自动切身份**
│   └── manual.py       首版实现:显式切换 + 访客兜底
├── voice/              语音
│   ├── stt.py          sherpa-onnx 流式 Zipformer(CPU)
│   ├── tts.py          本地 vits-zh-ll(sherpa-onnx)
│   └── remote_tts.py   Qwen3-TTS 远端服务适配(当前主用)
└── tests/              4 个测试,共 88 项断言,全部无需硬件

services/
├── tts_server.py       Qwen3-TTS 服务(跑在 ~/tts-qwen,独立 venv)
└── bot_server.py       Pipecat SmallWebRTC 服务端(实验,见下)

client/
└── test_client.py      自写 aiortc 客户端(实验用,也是 N5095 客户端的雏形)

p0/                     P0 阶段的验证与基准脚本(保留用于复现)
├── verify_deepseek.py  DeepSeek 四条路径
├── verify_local_llm.py 本机 Qwen3.8-27B
└── stt_tts_bench/      STT/TTS 基准、模型下载脚本、线程扫描

测试

# 服务器上(完整)
cd ~/ai-companion
.venv/bin/python app/tests/test_llm.py          # 27 项
.venv/bin/python app/tests/test_memory.py       # 28 项
.venv/bin/python app/tests/test_identity.py     # 17 项
.venv/bin/python app/tests/test_engine_e2e.py   # 16 项,端到端

端到端测试不需要麦克风、音箱、摄像头、ESP32—— 这是"服务器即助手"架构的直接好处:链路可以在任何有 CPU 的地方验证, 硬件到货后再接最后一段。跑完会把合成音频写到 out/,可以人工试听。

设计上几个刻意的选择

接口按最坏情况设计。 MemoryStore 全是 async、可超时、可失败, 虽然现在背后是本地 SQLite(微秒级)。因为将来多设备共享记忆时要搬到服务器, 如果现在写成同步直连本地文件,那时要重写所有调用方。

身份、会话、视觉追踪三者分开。 user_id(永久身份)、session_id(一次会话)、 track_id(画面里的一个目标)互不冒充。人脸认出来不等于确认对方在说话—— 所以人脸只产生"建议",不自动切换身份。做错的后果是路人经过就把你的私人记忆念出来。

记忆查询强制带 owner_user_id。 没有"按 memory_id 单独取"的口子: 猜到 id 就能读别人的记忆,是这类系统最容易犯的错。删除同理,必须同时匹配 owner。

自动抽取的记忆是"候选",默认不参与检索。 只有用户显式"记住"或工具调用 写入的才是"已确认"。对应方案 §8 的"模型推测不当作确认事实"。

异步任务绑定创建时的身份。 回复后写记忆的任务在创建时捕获 user_id, 不读"当前用户"——否则用户在这几百毫秒内切了身份,这条记忆会写到别人名下。

引擎与传输解耦。 不依赖 Pipecat、不依赖任何传输方式,只依赖四个接口。 好处:可以用文件音频端到端测试;接 WebRTC 时只需写薄适配层。

当前卡点:Pipecat 传了但没接上

方案文档写"Pipecat 作为唯一会话管线框架",但代码里它只在 requirements.txt 里。

2026-09-15 做了传输层实验(services/bot_server.py + client/test_client.py):

  • ✅ 传输层跑通:信令、ICE、data channel、双向媒体轨
  • ❌ 帧路由未通:自定义处理器的 process_frame 调用次数为 0, 连自己注入的 StartFrame 都没进去;引擎从未运行

现象是"连接全对、音轨协商成功、但助手永远不说话,且日志里一条错误都没有"。

这直接影响 A/B 架构方案:A 是"Pipecat 只做传输、逻辑归自有引擎", B 是"按 Pipecat 的 service 模型重构"。帧送不进自定义处理器, 说明 A 的核心假设不是开箱即用的。

根因未定位,两种可能必须区分:接线错误(接缝浅,A 可行) vs 模型不兼容(接缝深,应转 B)。

完整记录见代码仓库外的 P0-Pipecat传输层实验报告.md(在 Obsidian 笔记库里)。

实验挖出的 8 个坑

每个都表现为"什么都没发生",极难排查:

  1. Pipecat 客户端 SDK 不覆盖"Linux x86 盒子接 USB 阵列"这种形态 → 客户端得自己写
  2. 客户端必须建 data channel 并定期发 "ping",否则服务端一帧音频都不推
  3. offer 里没有媒体时 aiortc 完全不收集 ICE 候选(连连通性测试都要加静音轨)
  4. ICE 收集完成前发 offer → SDP 里 0 个候选地址
  5. PipelineTask.run() 要 WorkerParams(task_manager=...),不是 PipelineParams
  6. TaskManager() 必须在事件循环内创建
  7. 后台任务里的异常被静默吞掉,日志里查不到
  8. 信令回调里必须 create_task,不能 await task.run()

踩过的坑

全部写在对应文件的注释里,这里只列索引,详见 部署说明.md §已知的坑:

  • 本机 vLLM 只接受一条 system 消息(多条 400)
  • 思考字段名 vLLM 用 reasoning、DeepSeek 用 reasoning_content
  • DeepSeek 默认开思考,多轮/工具回灌会 400;且思考会吃掉 max_tokens
  • max_num_sentences=1 静默截断多句输入
  • VITS 非自回归,首音 = 整句合成耗时
  • Qwen3-TTS 短句 RTF 反而最差(固定开销约 1 秒)
  • qwen-tts 与 vllm-omni 依赖互斥,必须分 venv
  • STT 与 TTS 的线程扩展方向相反
S
Description
No description provided
Readme
418 KiB
Languages
Python 96.8%
C++ 2%
Shell 1.2%