AI桌面陪伴助手:方案更新至 v2.4,代码剥离到独立仓库
代码剥离 - 助手本体、TTS 服务、P0 基准脚本、ESP32 固件移到 ~/workspace/ai-companion (独立 git 仓库),本目录只留方案、P0 报告与硬件记录 - 因此删除 esp32_smile_test.ino / esp32_touch_eyes.ino - 同时移除已失效的 .gitignore(原本只为挡住模型体积,代码已搬走) 方案 v2.4 重大变更:架构从"单机"改为"服务器即助手、本地为客户端" - 起因:实测 N5095 无 AVX2,跑不动可用的语音模型 - N5095 降为客户端/外设桥,只做 USB 接入、DoA、WebRTC 客户端 - 实施阶段表加进度列(P0/P1/P3/P4a 部分完成),新增三条验收要求 - 采购清单按实际到货更新,标出屏幕形态待确认(2.8寸 vs 原定1.28寸圆屏) 技术栈(实测更新) - LLM:DeepSeek deepseek-flash(云端,必须关思考);本机 27B 已停用 - TTS:Qwen3-TTS 1.7B(服务器 3090);vits-zh-ll 因音色不满意被替换 - STT:Zipformer int8 未变,中文识别率仍是未闭合项 新增两份 P0 报告 - P0-Qwen3TTS报告.md:TTS 换型过程与实测 - P0-DeepSeek验证报告.md:四条路径验证 两份旧报告加了状态说明(结论仍有效但选型已变), 并修正了随代码移走而失效的引用与复现命令。
This commit is contained in:
@@ -0,0 +1,228 @@
|
||||
# P0 基准报告 · 本地 STT / TTS
|
||||
|
||||
> ⚠️ **TTS 选型已于 2026-09-15 变更。**
|
||||
> 本报告选出的 vits-zh-ll 虽然速度达标(服务器 RTF 0.09),
|
||||
> 但用户试听反馈**音色不满意**,最终换成 Qwen3-TTS 1.7B(RTF 0.55–0.94)。
|
||||
> 见 `P0-Qwen3TTS报告.md`。
|
||||
>
|
||||
> **本报告的核心结论仍然有效且在用**:
|
||||
> - STT 线程数不能按"核多给多"猜(N5095 上 4 线程比 2 线程慢 3.9 倍)
|
||||
> - 冲刷耗时 ≈ 0,流式解码已持续输出
|
||||
> - VITS 非自回归、`max_num_sentences=1` 会静默截断——这两条对 Qwen3-TTS 同样适用
|
||||
> - STT 中文识别率未测(无参考文本),此结论仍然成立
|
||||
>
|
||||
> 另:本报告测出的"服务器(i9-14900K)上 STT RTF 0.03"仍是当前生产配置。
|
||||
|
||||
日期:2026-09-14。机器:Celeron N5095(4 核,**无 AVX2**)/ Ubuntu 24.04.3 / 16G。
|
||||
脚本目录:`p0/stt_tts_bench/`。全部为实测数据,非估算。
|
||||
|
||||
复现:
|
||||
|
||||
```bash
|
||||
cd ~/workspace/ai-companion/p0/stt_tts_bench
|
||||
./download_models.sh # 走 hf-mirror
|
||||
python3 bench_stt.py # STT
|
||||
python3 bench_tts_compare.py # TTS 横向对比
|
||||
python3 bench_concurrent.py # 并发
|
||||
python3 bench_roundtrip.py # 往返
|
||||
python3 bench_threads.py # 线程扫描
|
||||
```
|
||||
|
||||
## 一、结论
|
||||
|
||||
**本地语音方案成立,但必须换 TTS 模型。**
|
||||
|
||||
内核:**STT 没问题,vits-zh-ll 不行,Piper 可以。**
|
||||
|
||||
| 组件 | 选型 | RTF | 判定 |
|
||||
|---|---|---|---|
|
||||
| STT | 流式 Zipformer 中英双语 int8 | 0.22(单跑)/ 0.50(并发) | ✅ 可用 |
|
||||
| TTS | vits-zh-ll(115MB) | **1.86** | ❌ 慢于实时,不可用 |
|
||||
| TTS | **piper-huayan-medium(61MB)** | **0.36** | ✅ 推荐 |
|
||||
| TTS | piper-huayan-x_low(20MB) | 0.17 | ⚠ 快但音质存疑,待试听 |
|
||||
|
||||
## 二、STT
|
||||
|
||||
### 速度
|
||||
|
||||
| 线程数 | RTF 中位 | 单块解码 p95 | 判定 |
|
||||
|---|---|---|---|
|
||||
| 1 | 0.37 | 1.19 | 平均够,但存在突发积压 |
|
||||
| **2** | **0.22** | **0.79** | ✅ 最佳 |
|
||||
| 3 | 0.63 | — | 反而变慢 |
|
||||
| 4 | 0.86 | 3.78 | 严重变慢 |
|
||||
|
||||
**关键发现:线程数超过 2 会急剧退化**,2→4 线程 RTF 从 0.22 涨到 0.86(慢 3.9 倍)。
|
||||
重复 5 轮稳定复现,不是噪声。
|
||||
|
||||
原因判断:N5095(Jasper Lake)是**单通道内存**,且 L2 仅 1.5MB、L3 仅 4MB。
|
||||
并且 onnxruntime 空闲工作线程默认自旋占核;4 个 ORT 线程 + Python 主线程 = 5 线程抢 4 个核,
|
||||
主线程(负责喂音频)被饿死。
|
||||
|
||||
→ **实现时 STT 固定 `num_threads=2`。** 不要"核多就多给线程"。
|
||||
|
||||
### 其他指标
|
||||
|
||||
- **首字延迟**:喂进 0.5~1.4 秒音频后出现第一个中间结果。这是模型的算法前瞻量,与算力无关。
|
||||
- **冲刷耗时 = 0ms**:`input_finished()` 后立即拿到最终结果。流式解码过程中已经持续输出,
|
||||
收尾不额外花钱。**"说完到出结果"的开销主要在 VAD 判定说完的等待,不在识别器。**
|
||||
- 注:并非每个 100ms 块都触发解码(约每 3 块解一次,即模型 chunk 约 300ms),
|
||||
所以"单块 p95 > 1"不等于积压,应看 RTF。
|
||||
|
||||
### 识别正确率:未测
|
||||
|
||||
4 段测试音频是 sherpa-onnx 自带的中英双语真人录音,**没有参考文本**,无法算字错率。
|
||||
|
||||
尝试用"TTS 合成已知文本 → 喂给 STT"来算 CER,但**这个方法在这台机器上不成立**:
|
||||
合成音频会让 STT 产生大量重复("晴晴转转"、"月月月"),而同样的解码配置下真人录音完全正常。
|
||||
换 greedy / beam_search、换块大小、开关端点检测,四种配置结果完全一致 —— 说明问题出在合成音频
|
||||
本身,不是解码配置。
|
||||
|
||||
**结论:STT 在中文真人语音上的正确率目前没有可信数据。** 需要一份带标注的中文真人测试集,
|
||||
或用麦克风录一批带文本的语音。这是 P0 尚未闭合的一项,不能靠现有数据下结论。
|
||||
|
||||
## 三、TTS
|
||||
|
||||
### 速度
|
||||
|
||||
| 模型 | 大小 | RTF(2线程) | 19字句首音延迟 | 判定 |
|
||||
|---|---|---|---|---|
|
||||
| vits-zh-ll | 115MB | 1.86 | 2748ms | ❌ |
|
||||
| piper-x_low | 20MB | 0.17 | 585ms | ✅ 快 |
|
||||
| piper-huayan-medium | 61MB+espeak | 0.36 | ~650ms | ✅ 推荐 |
|
||||
|
||||
**VITS 非自回归**:验收时确认回调只在合成完成后触发一次,
|
||||
所以**首音延迟 = 总合成耗时**,没有"边合成边播"的可能。
|
||||
分句播放是唯一的提前出声手段(`max_num_sentences=1` 逐句合成)。
|
||||
|
||||
### 连续播放模拟(5 句)
|
||||
|
||||
| 模型 | 各句空档 | 最大空档 |
|
||||
|---|---|---|
|
||||
| vits-zh-ll | 2.0 / 1.6 / 0.5 / 2.9 / 3.8 秒 | **3.80s** |
|
||||
| piper-x_low | 0.5 / 0 / 0 / 0 / 0 秒 | 0.47s(仅首句) |
|
||||
|
||||
vits-zh-ll 每句之间都会出现 0.5~3.8 秒的静默空档,说话断断续续。
|
||||
Piper 除首句外无空档。
|
||||
|
||||
### 音质:必须人工试听
|
||||
|
||||
往返测试的 CER 不可信(见上),但可以给出**相对**信号:
|
||||
|
||||
| 模型 | 往返 CER | 输出峰值 | 输出 RMS |
|
||||
|---|---|---|---|
|
||||
| vits-zh-ll | 0.25 | **0.15** | 0.045 |
|
||||
| piper-x_low | 0.72 | 0.49 | 0.092 |
|
||||
| piper-huayan-medium | 0.32 | 0.59 | 0.105 |
|
||||
|
||||
两点值得注意:
|
||||
|
||||
1. **vits-zh-ll 输出音量极低**(峰值 0.15,只有 Piper 的 1/4)。接音箱时必须补增益,
|
||||
否则要么听不见,要么底噪被一起放大。
|
||||
2. **piper-x_low 音质存疑**:合成时有 `Unknown token: ̪` 警告,且往返 CER 明显偏差。
|
||||
速度虽好(RTF 0.17),但**不建议直接用**。
|
||||
|
||||
已生成三个模型同一句话的试听文件,请人工判断:
|
||||
|
||||
```
|
||||
~/workspace/ai-companion/p0/stt_tts_bench/listening_test/vits-zh-ll.wav
|
||||
p0/stt_tts_bench/listening_test/piper-x_low.wav
|
||||
p0/stt_tts_bench/listening_test/piper-medium.wav
|
||||
```
|
||||
|
||||
**音质只能靠耳朵定,仪器测不出来。这一项在试听前不能算通过。**
|
||||
|
||||
## 四、并发(这是最接近真实负载的测试)
|
||||
|
||||
助手说话时麦克风不关(要检测打断),所以 STT 必须与 TTS 同时跑。
|
||||
|
||||
| TTS 模型 | STT 单跑 | TTS 单跑 | STT 并发 | TTS 并发 | 退化 | 判定 |
|
||||
|---|---|---|---|---|---|---|
|
||||
| piper-x_low | 0.26 | 0.19 | **0.50** | **0.33** | 1.9× / 1.7× | ✅ 余量 2.0× / 3.1× |
|
||||
| vits-zh-ll | 0.26 | 1.97 | 0.48 | **2.39** | 1.9× / 1.2× | ❌ 跟不上 |
|
||||
|
||||
并发下两者都退化约 1.8 倍(4 核争抢所致,符合预期)。
|
||||
用 Piper 时 STT 0.50 + TTS 0.33,都远低于 1,**架构成立**。
|
||||
|
||||
但这只是 STT + TTS 两项。方案还要同时跑人脸检测/跟踪。视觉再加进来是否还够,
|
||||
需要单独测(未测)。
|
||||
|
||||
## 五、对方案 P1 指标的影响
|
||||
|
||||
方案 P1 目标:"短问答结束到首段有声回复,P50≤2 秒、P95≤4 秒"。
|
||||
|
||||
用实测数据推算(`thinking` 关闭):
|
||||
|
||||
| 环节 | 耗时 | 来源 |
|
||||
|---|---|---|
|
||||
| VAD 判定说完 | 200~500ms(估) | 待实测 |
|
||||
| STT 冲刷 | ≈0ms | 实测 |
|
||||
| LLM 首 token | 786ms(P50) | P0-DeepSeek 报告 |
|
||||
| TTS 首音(Piper medium,短句) | ~650ms | 实测 |
|
||||
| **合计** | **约 1.6~2.0 秒** | |
|
||||
|
||||
**用 Piper 时 P50 目标勉强可达**,但没有富余;P95 需要看网络抖动。
|
||||
用 vits-zh-ll 则完全不可能(TTS 单项就 2.7 秒)。
|
||||
|
||||
## 六、未测事项(不要当成已通过)
|
||||
|
||||
- **STT 中文真人识别率**:无参考文本,未测。
|
||||
- **TTS 音质**:待人工试听 `listening_test/`。
|
||||
- **视觉并发**:人脸检测加入后的 CPU 争抢未测。
|
||||
- **真实音频链路**:麦克风阵列、远场、AEC、回声参考全部未测,无硬件。
|
||||
- **int8 量化损失**:只测了 int8 encoder,未与 fp32 对照(fp32 315MB,本机大概率跑不动)。
|
||||
- **长时稳定性**:未做 2 小时/8 小时测试。
|
||||
|
||||
## 六之二、补充:更多 TTS 候选(同日追加)
|
||||
|
||||
用户试听后反馈 **vits-zh-ll 听感最好**,但它是 RTF 1.86 的不可用项。
|
||||
据此追加测试了 5 个候选,全部实测:
|
||||
|
||||
| 模型 | 大小 | 单独 RTF(最佳线程) | 并发 RTF | 备注 |
|
||||
|---|---|---|---|---|
|
||||
| piper-x_low | 20MB | 0.17 @2 | 0.33 | 有 Unknown token 警告,音质存疑 |
|
||||
| **piper-medium** | 61MB | **0.32 @2** | **0.51** | 用户反馈不如 vits |
|
||||
| **matcha-icefall-zh-baker** | 88MB + 3.6MB 声码器 | **0.38 @2** | **0.94** | 新候选,流匹配 + HiFi-GAN |
|
||||
| vits-zh-ll | 115MB | 1.14 @4 / 1.87 @2 | 1.48–2.39 | 听感最好,但不可用 |
|
||||
| kokoro fp32 | 383MB | 1.96 @4 | — | 排除 |
|
||||
| kokoro int8 | 181MB | **3.54 @4** | — | 排除 |
|
||||
|
||||
### 两条新增的机理结论
|
||||
|
||||
1. **TTS 的线程扩展性与 STT 相反**。STT 超过 2 线程急剧退化(4线程慢 3.9 倍),
|
||||
而 vits-zh-ll 是线程越多越快(1线程 3.59 → 4线程 1.14)。
|
||||
原因:STT 是大量小算子(每算子一次线程同步,线程越多同步开销越大),
|
||||
TTS 是少数大算子(CPU 密集,可有效并行)。
|
||||
**两类服务的线程配置必须分开调,不能统一。**
|
||||
|
||||
2. **int8 量化在这台机器上反而更慢**。Kokoro int8(3.54)比 fp32(1.96)慢近一倍。
|
||||
原因:Tremont 没有 VNNI 指令,int8 的快速点积路径用不上,
|
||||
权重需要现场反量化,额外开销盖过了带宽收益。
|
||||
→ **本机不要走 int8 量化路线做加速。**(STT 的 int8 encoder 是发布方直接训练的量化模型,
|
||||
不是运行时量化,不受此限。)
|
||||
|
||||
### 当前选型状态
|
||||
|
||||
- **速度与余量最优**:piper-medium(并发 0.51,余量约 2 倍)
|
||||
- **架构档次更高、速度尚可**:matcha-baker(并发 0.94,**几乎没有余量**)
|
||||
- matcha 的并发退化(0.38→0.94,2.5 倍)明显大于 piper-medium(0.32→0.51,1.6 倍),
|
||||
判断与 HiFi-GAN 声码器吃内存带宽有关(本机单通道内存)。
|
||||
**若选 matcha,人脸检测等其它负载再加进来很可能压垮实时性,需要实测。**
|
||||
|
||||
已生成响度归一化的 A/B 试听(统一到 -20 dBFS RMS,
|
||||
避免"响度差被误当成音质差"——vits-zh-ll 原始峰值仅 0.17,比 Piper 低 3.5 倍):
|
||||
|
||||
```
|
||||
~/workspace/ai-companion/p0/stt_tts_bench/listening_ab/{vits-zh-ll,matcha-baker,piper-medium}_{0,1}.wav
|
||||
```
|
||||
|
||||
## 七、工程陷阱记录
|
||||
|
||||
1. **`max_num_sentences=1` 会截断多句输入**为第一句,且不报错。
|
||||
传含多个句号的文本会静默丢失后半段。必须自己先分句逐句送。
|
||||
2. **hf-mirror 对 urllib 默认 UA 返回 403**,需伪装 User-Agent。
|
||||
3. **espeak-ng-data 里有带空格的文件名**(`voices/!v/Mr serious`),URL 必须转义。
|
||||
4. **代码曾位于 note_sync(Obsidian 笔记仓库)内,是经软链接访问的**,
|
||||
模型一度差点被推进笔记仓库。**2026-09-15 已把代码剥离为独立仓库**
|
||||
(`~/workspace/ai-companion/`),笔记库只剩文档,这个问题不复存在。
|
||||
教训:代码和笔记共用版本控制,迟早要在 ignore 规则上反复花时间。
|
||||
Reference in New Issue
Block a user