从 Obsidian 笔记库剥离为独立仓库。规划文档与 P0 报告留在笔记库, 代码(助手本体、TTS 服务、P0 基准脚本、ESP32 固件)在此。 架构:服务器是助手,本地是客户端 - 助手本体跑在 cadadmin-server,N5095 只做 USB 外设桥与网络桥接 - 起因:实测 N5095 无 AVX2,跑不动可用的语音模型 当前技术栈 - LLM:DeepSeek deepseek-flash(云端,必须关思考) - TTS:Qwen3-TTS 1.7B(服务器 3090,独立 HTTP 服务) - STT:Zipformer 中英双语 int8(CPU,中文识别率未测) 实测:端到端 16/16 通过,首音延迟 1545ms (对比:本地 LLM + vits-zh-ll 是 179ms,但音色不满意——音质与延迟 在此硬件上此消彼长) 设计要点 - MemoryStore 接口按"可能远程"设计(全 async、可超时、可失败) - user_id / session_id / track_id 三者分开,人脸只作候选不自动切身份 - 记忆查询强制带 owner_user_id,无按 id 单独取的口子 - 自动抽取的记忆是"候选",默认不参与检索 - 异步写记忆绑定创建时的身份,不读"当前用户" - 引擎与传输解耦,只用文件音频即可端到端测试 模型与音频产物不进版本管理(1.1GB),由 download_models.sh 恢复
7.8 KiB
7.8 KiB
部署说明 · 服务器侧助手
更新:2026-09-15。状态:已部署并跑通,端到端测试 16/16 通过。
架构定位
cadadmin-server(192.168.0.20) N5095 ESP32-S3
助手本体 外设桥 表情/动作
├─ 会话引擎 ├─ USB 麦克风阵列 ├─ 屏幕
├─ LLM:DeepSeek 云 API ├─ USB 摄像头 ├─ 电机
├─ 记忆 SQLite ├─ DoA 读取(不过网) └─ 按键
├─ 身份裁决 ├─ 人脸检测
├─ STT:Zipformer(CPU) ├─ WebRTC 客户端
└─ TTS:Qwen3-TTS(3090) └─ 串口 → ESP32
手机 / 浏览器:额外客户端
"服务器是助手,本地是客户端"(2026-09-14 确定)。 助手本体、记忆库、语音模型全部在服务器;N5095 只做 USB 外设接入、 DoA 读取和网络桥接,不跑任何推理。
当前技术栈
| 环节 | 方案 | 位置 | 实测 |
|---|---|---|---|
| LLM | DeepSeek deepseek-flash(关思考) |
云 API | 首 token 0.8–1.2s |
| TTS | Qwen3-TTS 1.7B-CustomVoice | 3090 | RTF 0.55–0.94 |
| STT | Zipformer 中英双语 int8 | CPU 4 线程 | RTF 0.03 |
| 记忆 | SQLite | 服务器本地 | — |
| 身份 | 手动切换 + 访客兜底 | 服务器 | — |
首音延迟约 1.5 秒(文本输入)。方案 P1 目标是 P50≤2 秒, 加上 VAD 判定后处在 2.0–2.2 秒,刚好卡线,没有余量。
为什么 LLM 走云端而 TTS 在本地
两者都需要算力,但 3090 只有 24GB:
| 方案 | 显存 | 首音延迟 |
|---|---|---|
| 本地 Qwen3.8-27B + 本地 vits-zh-ll | 27B 占 21.5GB | 179ms(音色不满意) |
| DeepSeek + 本地 Qwen3-TTS | TTS 占 8.9GB | 1545ms |
2026-09-15 选了后者:用约 600ms 的网络往返 + 约 900ms 的 TTS 固定开销, 换音质从"用户不满意"到"用户试听选定"。27B 的 vLLM 已停。
这是可逆的(改 AI_LLM_PRIMARY 即可切回),但切回去 GPU 就不够跑 Qwen3-TTS。
服务器上的布局
~/ai-companion/ 助手主体
├── app/ 24 个 py 文件
│ ├── config.py 配置,支持环境变量覆盖
│ ├── prompts.py 单条 system 每轮重建
│ ├── engine.py 会话引擎(身份→记忆→LLM→TTS)
│ ├── llm/ 后端抽象 + vLLM + DeepSeek + 降级路由
│ ├── memory/ 接口 + SQLite 实现
│ ├── identity/ 接口 + 手动切换实现
│ ├── voice/ STT / TTS(本地 + 远端两种后端)
│ └── tests/ 4 个测试,共 88 项断言
├── models -> ~/ai_companion_bench/models 软链,未重复占用
└── .venv/ 608MB(Python 3.12)
~/tts-service/ Qwen3-TTS 模型(4.3GB)
~/tts-qwen/ Qwen3-TTS 服务
├── tts_server.py FastAPI 服务,OpenAI 兼容 /v1/audio/speech
├── samples/ 音色试听样本
└── .venv/ 独立 venv(与 vllm-omni 依赖互斥,必须分开)
~/ai_companion_bench/ P0 基准脚本与模型(STT/TTS/vits)
~/ai/qwen-serving/ 原来的 27B vLLM(已停,保留可随时启回)
配置
~/.config/ai_assistant/env(权限 600,含密钥)
| 变量 | 服务器值 | 说明 |
|---|---|---|
AI_LLM_PRIMARY |
deepseek |
主 LLM;改 local 可切回本机 vLLM |
AI_TTS_BACKEND |
remote |
remote=Qwen3-TTS 服务;local=vits-zh-ll |
TTS_SERVICE_URL |
http://127.0.0.1:18040 |
TTS 服务地址 |
AI_TTS_SPEAKER |
Serena |
用户试听选定 |
AI_TTS_INSTRUCTIONS |
用轻快活泼的语气说 |
风格指令 |
AI_STT_THREADS |
4 |
N5095 上必须改成 2(见下) |
AI_MODEL_DIR |
/home/yonglin/ai-companion/models |
两边模型位置不同 |
两台机器的线程配置不能照抄:N5095(4核无AVX2)STT 超过 2 线程急剧退化 (4 线程慢 3.9 倍);服务器(i9-14900K 有AVX2)给 4 线程没问题。 TTS 的线程扩展方向又和 STT 相反(越多越快)。
启动
# 1. TTS 服务(模型常驻显存,约 4 秒加载完)
cd ~/tts-qwen && setsid nohup .venv/bin/python tts_server.py --port 18040 \
> logs/tts_service.log 2>&1 < /dev/null &
# 2. 确认就绪
curl -s http://127.0.0.1:18040/health
# 3. 跑测试
cd ~/ai-companion && .venv/bin/python app/tests/test_engine_e2e.py
助手本体还没有常驻服务(没有 systemd),目前靠直接跑脚本。
部署代码
cd ~/workspace/ai-companion
rsync -az --delete --exclude '__pycache__' --exclude '*.pyc' \
app/ yonglin@cadadmin-server:~/ai-companion/app/
已知的坑
都已写进代码注释,这里只列索引。
接口层
- 本机 vLLM 只接受一条 system 消息,多条 400。记忆不能作为第二条 system 注入,必须并进同一条每轮重建。
- 思考字段名各实现不同:vLLM 用
reasoning,DeepSeek 用reasoning_content。 适配层两个都认,少认一个会静默丢弃思考内容。 - DeepSeek 默认开思考,此时要求把上一轮的
reasoning_content原样回传, 否则多轮/工具回灌一律 400。主路径必须显式关闭。 - 开思考会吃掉
max_tokens预算,实测 200 预算下回答只吐出两个字。
语音层
max_num_sentences=1会静默截断多句输入(sherpa-onnx VITS), 必须自己先分句。- VITS 是非自回归的,没有"边合成边播",首音 = 整句合成耗时。 提前出声的唯一手段是按语义短句拆分。
- Qwen3-TTS 短句 RTF 反而最差(0.94 vs 长句 0.55):每次调用有约 1 秒 固定开销,与文本长度无关。所以"让模型先说个短句"并不划算。
记忆层
- 检索兜底要限量:关键词全不命中时退回最近记忆,但只在记忆总数 ≤10 时 生效。不加限制就变成"每轮全量注入",不再是按需检索。
- "被引用的 user 必须存在"由存储层自己保证(
_ensure_user_row), 不能让每个调用点各自记得——漏一处就报FOREIGN KEY constraint failed。
依赖层
- qwen-tts 与 vllm-omni 依赖互斥(transformers 4.57 vs 5.14、 hf_hub 0.36 vs 1.31)。装同一个 venv 会互相破坏,实测把 vllm-omni 搞崩过。 所以 TTS 跑独立进程 + HTTP。
- flash-attn 装不上:预编译 wheel 最高只支持 torch 2.9,本机是 2.14 (vLLM 0.28 拉上来的);源码编译要从 GitHub 拉依赖,而这台机器下 GitHub release 会断连。
未做的事
- 没有常驻服务:TTS 服务和助手都没有 systemd 开机自启(方案 §10 要求 P2 后做)
- 没接 Pipecat 管线:引擎与传输解耦,接 WebRTC 时写薄适配层即可
- 设备侧代码不存在:N5095 的外设桥(USB、WebRTC 客户端、串口)尚未开始
- STT 没换:仍是 2023 年的 Zipformer int8,中文识别率从未测过
- 记忆库在服务器上,而人脸特征按方案必须留在设备端——将来做 "删除我的数据"要跨两台机器协调,这个数据边界问题需要单独想
- 代码仍在 note_sync 笔记仓库里,未拆成独立仓库
相关文档
AI桌面陪伴助手方案.md— 总体方案P0-语音基准报告.md— STT/TTS 基准(vits 时代,含线程数结论)P0-DeepSeek验证报告.md— DeepSeek API 四条路径P0-本地LLM验证报告.md— 本机 Qwen3.8-27B(已停用)P0-Qwen3TTS报告.md— Qwen3-TTS 选型与部署