代码剥离 - 助手本体、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:四条路径验证 两份旧报告加了状态说明(结论仍有效但选型已变), 并修正了随代码移走而失效的引用与复现命令。
11 KiB
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/。全部为实测数据,非估算。
复现:
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 |
两点值得注意:
- vits-zh-ll 输出音量极低(峰值 0.15,只有 Piper 的 1/4)。接音箱时必须补增益, 否则要么听不见,要么底噪被一起放大。
- 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 | — | 排除 |
两条新增的机理结论
-
TTS 的线程扩展性与 STT 相反。STT 超过 2 线程急剧退化(4线程慢 3.9 倍), 而 vits-zh-ll 是线程越多越快(1线程 3.59 → 4线程 1.14)。 原因:STT 是大量小算子(每算子一次线程同步,线程越多同步开销越大), TTS 是少数大算子(CPU 密集,可有效并行)。 两类服务的线程配置必须分开调,不能统一。
-
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
七、工程陷阱记录
max_num_sentences=1会截断多句输入为第一句,且不报错。 传含多个句号的文本会静默丢失后半段。必须自己先分句逐句送。- hf-mirror 对 urllib 默认 UA 返回 403,需伪装 User-Agent。
- espeak-ng-data 里有带空格的文件名(
voices/!v/Mr serious),URL 必须转义。 - 代码曾位于 note_sync(Obsidian 笔记仓库)内,是经软链接访问的,
模型一度差点被推进笔记仓库。2026-09-15 已把代码剥离为独立仓库
(
~/workspace/ai-companion/),笔记库只剩文档,这个问题不复存在。 教训:代码和笔记共用版本控制,迟早要在 ignore 规则上反复花时间。