Files
ai-companion/README.md
T
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

129 lines
6.6 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.
# 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 基准、模型下载脚本、线程扫描
```
## 测试
```bash
# 服务器上(完整)
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 的线程扩展方向相反