#移远产品体验官 Quectel Pi M2 实战:离线 TTS 从 11 秒压到 0.4 秒

发布时间:2026/10/23 5:05:33
#移远产品体验官 Quectel Pi M2 实战:离线 TTS 从 11 秒压到 0.4 秒 环境Quectel Pi M2瑞芯微 RK35764×A722.2GHz 4×A532.0GHz6 TOPS NPU3.8 GB RAM 无 swap/ Debian 13 (trixie) aarch64内核 6.1.118-rt36目标把「完全离线中文语音助手」的语音播报延迟压到能自然对话的程度关键词sherpa-onnx、VITS、Kokoro、Matcha、Vocos、RKNN、RK3576、RTF、首声延迟、NPU 声码器前言这是 Quectel Pi M2 离线语音助手系列实践的一篇。前面的链路已经打通第一篇板载麦克风录音 蓝牙音箱播放第二篇中文语音唤醒KWS第三篇本地大模型 RKLLM 在 NPU 上跑通0.5B 26~29 tok/s本篇是第四篇离线 TTS 低延迟优化。说明ASRParaformer 意图工具层 整机闭环 那篇还没发放到后面单独写。本篇只借用它的时延数据做整机预算不展开讲那部分的实现。链路是唤醒(KWS) → VAD 录音 → ASR(Paraformer) → LLM(RKLLM/NPU) → TTS → 播放唤醒、ASR、LLM 都很快唯独TTS 一开口就露馅用户说完话板子要沉默 8~11 秒才出声一句 10 秒的回答要合成 17 秒。这篇文章记录了我把这块硬骨头啃下来的全过程四条技术路线、每一版的实测数字、以及最后落地的方案。先说结论方便你判断要不要往下读方案计算单元端到端 RTF首声延迟结论VITS aishell3 int8整句合成CPU1.7 ~ 1.98 ~ 11 s基线不可用VITS 切句流水线CPU1.55 ~ 1.752.5 ~ 3.5 s能用但块间有静音空档Kokoro int8CPU3.6 ~ 4.0更慢排除原厂 RKNN TTS / MMS-TTSNPU——flow 算子无法有效加速排除Matcha VocosRKNNNPU0.099 ~ 0.1040.37 ~ 0.43 s最终采用最终效果整机链路ASR 0.18 s → LLM 0.60 s → TTS 首声 0.41 sRTF 0.14 ~ 0.18合成速度约为实时播放的 6~7 倍首声比原来的 VITS 快了20 倍以上。一、先把指标定义清楚聊 TTS 性能如果只盯「一句话合成要几秒」很容易被带偏。本文统一用三个指标指标定义为什么重要首声延迟文本就绪 → 第一个音频帧进入播放器的耗时用户体感「卡不卡」的唯一指标RTF合成耗时 ÷ 音频时长 1 才能跟上实时播放 1 一定积压块间空档分段合成时上一段播完到下一段出声的间隔决定「听起来像不像机器人喘气」关键认知VITS / Matcha 这类非流式声学模型必须把一整段文本合成完才能出声。所以对整句合成来说首声延迟 ≈ 整段音频时长 × RTFRTF 1.7 意味着「说 10 秒的话要先算 17 秒」。这就是 8~11 秒沉默的数学解释 ——不是哪一行代码慢是整段合成 算力不足叠加的结果。二、基线sherpa-onnx VITS aishell3CPU最初用的就是 sherpa-onnx 官方现成方案vits-zh-aishell3.int8.onnxCPU 推理。importsherpa_onnx cfgsherpa_onnx.OfflineTtsConfig(modelsherpa_onnx.OfflineTtsModelConfig(vitssherpa_onnx.OfflineTtsVitsModelConfig(modelvits-aishell3.int8.onnx,lexiconlexicon.txt,tokenstokens.txt,noise_scale0.667,noise_scale_w0.8,length_scale1.0,),providercpu,num_threads8,),rule_fstsdate.fst,number.fst,phone.fst,new_heteronym.fst,max_num_sentences1,)ttssherpa_onnx.OfflineTts(cfg)实测RK35768 线程22050 Hz 输出文本长度音频时长合成耗时RTF首声短句约 10 字2.5 s4.2 ~ 4.8 s1.7 ~ 1.94 ~ 5 s两三句回答约 40 字9 ~ 11 s16 ~ 20 s1.7 ~ 1.98 ~ 11 s我先把能调的参数都调了一遍num_threads4 / 6 / 8 线程RTF 从 1.9 只降到 1.78 线程以后不再有收益A72 只有 4 个超线程调度反而互相抢length_scale只是改变语速不改变 RTF锁频CPU governor 本来就是performance没有余量可挖。结论CPU 侧已经到顶瓶颈是模型结构和算力调参救不了。三、四条路线逐个实测3.1 路线 AKokoro int8 —— 名气大但在 RK3576 上更慢Kokoro 在 x86/手机端口碑很好sherpa-onnx 1.10.46 也已支持于是下载了 int8 版本约 140 MB实测方案线程RTF首声VITS aishell3 int881.55 ~ 1.758 ~ 11 sKokoro int883.6 ~ 4.0更慢RTF 直接翻倍排除。原因不复杂Kokoro 是为 x86/NEON 优化过的模型参数量和算子结构在 A72 上并不占优「在别的平台快」不等于「在 RK3576 上快」。教训端侧选型必须回到目标板子上实测社区口碑只能当参考。3.2 路线 B原厂 RKNN TTSMMS-TTS / rknn_model_zoo—— 方向对但算子不配合顺着「把 TTS 挪到 6 TOPS NPU 上」的思路去翻了 RK 原厂的rknn_model_zoo和社区里的 RKNN TTS 方案结论偏悲观VITS 结构的 flow归一化流算子在 RKNN 上无法有效加速。VITS 的耗时大头是 flow 里的多次耦合变换这类逐帧、小算子的结构在 NPU 上会被拆成大量碎片调度只有声码器HiFi-GAN那一段能吃到加速整体存在天花板。现成可直接用的中文 RKNN TTS 模型极少多数第三方模型只支持英文或者需要自己导出 ONNX、改动态 shape、再用匹配版本的rknn-toolkit2转换版本一错就加载失败。MMS-TTSVITS 结构同样受 flow 限制排除。这次调研没有白做它帮我锁定了正确的方向要绕开 flow选「非 flow 的声学模型 可全 NPU 加速的声码器」也就是下面的 Matcha Vocos。3.3 路线 C切句流水线还是 VITS纯工程优化在换模型之前先做了一个零成本的工程优化把长文本切成小句逐句合成、边合成边播放。defsplit_sentences(text,first10,hard18):按标点切小句后贪心打包成合成单元。 首块压到约 first 字因为 VITS 非流式首块越短出声越快 后续块放宽到 hard 字反正它们在播放期间后台合成。out,buf[],forcin_split_clauses(text):limithardifoutelsefirstifbufandlen(buf)len(c)limit:out.append(buf);buf,limit,hardwhilelen(c)limit:k_safe_cut(c,limit)out.append(c[:k]);c,limitc[k:],hard bufcifbuf:out.append(buf)returnout配合一个「后台合成线程 主线程顺序播放」的PipelinedSpeaker后台不断把句子合成成 wav主线程按序aplay。效果指标整句合成切句流水线首声8 ~ 11 s2.5 ~ 3.5 sRTF1.7 ~ 1.91.55 ~ 1.75不变块间空档无有约 0.2 ~ 0.8 s首声降下来了但RTF 仍大于 1意味着只要回答稍长播放就会「追不上」合成中间开始出现可听见的静音空档。这条路只能救急不能当终点。切句里有个细节值得抄走_safe_cut在切点前会回退避免把「2026年10月21日」这种数字串劈成两半也避免把英文单词切开。3.4 路线 DMatcha VocosRKNN最终方案架构rkvoice-stream0.2.0 提供了一套面向 RK3576 的 Matcha Vocos 后端数据流是文本 → 文本前端(CPU, 查表/音素化) → tokens → Matcha RKNN(NPU) → mel 频谱 → Vocos RKNN(NPU) → 线性谱 → ISTFT(CPU) → PCM 16 kHz和 VITS 最大的区别Matcha 是「条件流匹配」声学模型推理只需 1 步 ODE没有 VITS 那种多次 flow 变换Vocos 是纯卷积声码器整段可静态编译成 RKNN全部落到 NPU两者都导出成固定 shape避免动态 shape 在 RKNN 上的额外开销。板上的两个模型模型文件大小编译时固定 shape单次耗时Matcha 声学模型matcha-s64.rknn55.5 MBseq_len80, x_len64→ 599 帧 mel≈9.58 s293.6 msVocos 声码器vocos-16khz-600.rknn28.2 MB600 帧输入输出mag/x/y [1,513,600]54.7 ms单次推理计时rknn-toolkit-lite22.3.2 librknnrt2.3.2NPU 950 MHzmatcha-s64 : 293.6 ms (599 帧 ≈ 9.58 s 音频) vocos-600 : 54.7 ms ISTFT (CPU) : ~50 ms说明加载模型时那句Query dynamic range failed ... static shape RKNN model是正常警告静态 shape 模型本来就没有动态量化范围忽略即可。端到端实测指标数值RTF0.099 ~ 0.104约每秒音频 0.1 秒合成首声0.37 ~ 0.43 s相对 VITS 提升约 15 ~ 17 倍第一个坑尾部「静音 孤立杂音」固定窗口模型有个副作用推理时按固定帧数输出拿到音频后要按估算的 mel 帧数裁剪。而纯中文的帧数估计偏大约 30%~40%多出来的部分会被声码器渲染成「一段静音 末尾一小块杂音」。一开始我是用板载 Paraformer 回读合成音频来验音质的原样输出是你好我是本地的的语音助手三 今天天气不错出去走走吧嘿末尾的「三」「嘿」不是文本里的字而是固定窗口多估帧产生的残响。解决办法是按能量分段只保留最后一个足够长的语音段deftrim_tail(samples,sr,thr0.02,win0.10,keep0.15):剪掉尾部静音与孤立残响。nint(win*sr)iflen(samples)4*n:returnsamples frameslen(samples)//n rmsnp.sqrt((samples[:frames*n].reshape(frames,n)**2).mean(axis1))loudrmsthr runs,start[],Nonefori,vinenumerate(loud):ifvandstartisNone:startielifnotvandstartisnotNone:runs.append((start,i));startNoneifstartisnotNone:runs.append((start,frames))long_runs[rforrinrunsifr[1]-r[0]4]# ≥0.4 s 才算语音ifnotlong_runs:returnsamples endmin(len(samples),long_runs[-1][1]*nint(keep*sr))returnsamples[:end]踩坑记录第一版阈值设了0.01结果剪不掉 —— 因为那段杂音的 RMS 是 0.108反而高于阈值。改成「100 ms 窗 能量分段 只保留最后一个 ≥0.4 s 的语音段」才彻底干净。回读验证同一个 Paraformer内存共三千八百九十三兆 可用二千三百四十兆第二个坑别关掉切句刚上 Matcha 时我以为「合成这么快干脆整段一次性合成」于是把首块/块长都设成 1000 字。结果反而更差切句策略首声合成耗时音频时长整段firstchunk10001.15 s更长11.41 s估多按句first24, chunk400.41 s更短正常原因还是那句话非流式模型的首声 首块合成时间。首块越短出声越快Matcha 的 RTF 只有 0.124 个字的首块合成约 0.4 秒正好卡在体感「即时应答」的区间。3.5 四条路线横向汇总路线首声RTF音质结论VITS 整句8 ~ 11 s1.7 ~ 1.9好不可用Kokoro int8更差3.6 ~ 4.0一般排除原厂 RKNN TTS未落地——flow 算子受限排除VITS 切句2.5 ~ 3.5 s1.55 ~ 1.75好有块间空档仅救急Matcha Vocos0.37 ~ 0.43 s0.10好ASR 回读可懂采用四、落地时绕不过的两件事4.1 Python 版本系统只有 3.13而官方 RKNN wheel 只到 cp312板端系统 Python 是3.13.5而且没有pipapt源是空的/var/lib/apt/lists里只有 auxfiles装不了包没有gcc/make源码编译也走不通sudo需要密码。而rknn-toolkit-lite2官方 wheel 只有cp37 ~ cp312且内部是 cp312 编译的.so不是纯 Python 包换版本必炸。系统 Python 又不敢动一堆系统工具依赖它。最后的解法是装一个完全隔离的 CPython 3.12# python-build-standalone 的 install_only 包解压即用不碰系统 Python# cpython-3.12.1520261020-aarch64-unknown-linux-gnu-install_only.tar.gz约 53 MBmkdir-p/userdata/rkllm/py312tar-xzfcpython-3.12.1520261020-aarch64-unknown-linux-gnu-install_only.tar.gz\-C/userdata/rkllm/py312 --strip-components1P/userdata/rkllm/py312/bin/python3.12$P-mpipinstallsherpa-onnx1.10.46 numpy soundfile pyyaml rknn-toolkit-lite22.3.2关键点librknnrt.so是2.3.2、驱动0.9.8所以rknn-toolkit-lite2也必须锁2.3.2三个版本严格对齐整个环境装在/userdata与系统 Python 完全隔离不需要 root、不需要 Docker。4.2 espeak-ng没有 root 也要中英混说Matcha 的中文走词典lexicon.txt英文走 espeak-ng 音素化。板子上没有 root 装 deb但 deb 本质上就是个 tar 包# 解包不需要安装dpkg-deb-xlibespeak-ng1_1.52.0dfsg-5build1_arm64.deb /userdata/rkllm/espeak dpkg-deb-xlibpcaudio0_1.3-1build1_arm64.deb /userdata/rkllm/espeak dpkg-deb-xlibsonic0_0.2.0-13build2_arm64.deb /userdata/rkllm/espeak# 把 .so 拷到一个固定目录mkdir-p/userdata/rkllm/lib# libespeak-ng.so.1 / libpcaudio.so.0 / libsonic.so.0然后不要指望LD_LIBRARY_PATH子进程/不同加载时机容易失效直接在进程启动时按绝对路径预加载_ESPEAK_LIB/userdata/rkllm/libfor_soin(libpcaudio.so.0,libsonic.so.0,libespeak-ng.so.1):try:ctypes.CDLL(os.path.join(_ESPEAK_LIB,_so),modectypes.RTLD_GLOBAL)exceptOSError:passRTLD_GLOBAL让后续CDLL(libespeak-ng.so.1)rkvoice-stream 内部按 SONAME 加载直接命中已加载的库。验证Initialize rc22050 SetVoice(en-us) rc0 hello - həlˈoʊ Python - pˈaɪθən中英混说实测 RTF 0.071中英切换正常。踩坑libespeak-ng-libespeak1是个 2974 字节的过渡空包里面只有一个libespeak.so.1符号链接装上没有任何用。要装libespeak-ng1本文用的 1.52.0。五、最终技术路线5.1 整体架构┌──────────── CPU ────────────┐ ┌──────── NPU ────────┐ 麦克风 → KWS(小智小智) → VAD → Paraformer ASR → RKLLM(Qwen2.5-0.5B) → 回复文本 │ ┌────────────────────────┘ ▼ 切句(首块24字/后续40字) → 后台合成线程 │ Matcha RKNN ──▶ Vocos RKNN ──▶ ISTFT(CPU) │ trim_tail 剪尾 │ aplay 顺序播放5.2 关键代码把 Matcha 适配成 sherpa 风格的 TTS上层编排切句、流水线播放、写 wav本来是照着 sherpa-onnx 的generate()接口写的所以只需要一个薄适配层不用改整机代码classMatchaTTS:def__init__(self,model_dir/userdata/rkllm/rksv):sys.path.insert(0,/userdata/rkllm/rksv/site)os.environ.setdefault(TTS_MODEL_DIR,model_dir)os.environ.setdefault(MATCHA_MODEL,models/matcha-s64.rknn)os.environ.setdefault(VOCOS_MODEL,models/vocos-16khz-600.rknn)fromrkvoice_stream.backends.tts.matchaimportcreate_rknn_tts_backend self.enginecreate_rknn_tts_backend(model_dir)self.engine.load()self.sample_rate16000defgenerate(self,text,sid0,speed1.0):audio,_self.engine.synthesize(text,speedspeed)return_Speech(trim_tail(audio,self.sample_rate),self.sample_rate)5.3 切句参数按后端区分同一套切句逻辑VITS 和 Matcha 的最优块长完全不同ifargs.ttsmatcha:first_len,chunk_len24,40# RTF 0.1块大一点减少拼接感else:first_len,chunk_len10,18# RTF 1.7首块必须够短同时保留--tts vits回退开关换模型出问题时一行参数就能退回 CPU 方案。5.4 整机时延预算0.5B Matcha实测整机链路已经在板子上跑通ASR 与意图工具层的实现细节留到后续那篇这里只看时间花在哪。环节耗时ASRParaformer整句0.18 sLLM0.5B短回答含首 token0.60 sTTS 首声0.41 s合计说完话 → 听见第一个字约 1.2 s长回答两三句的 TTS 首声约 0.75 s因为首块仍然是 24 字。冷启动注意LLM 装载约 1.6 s、MatchaVocos 装载约 1.2 s、Paraformer 装载约 8.5 s。做常驻服务时只装一次不要在每轮对话里重新加载。5.5 怎么跑# 实时唤醒闭环默认 MatchaVocos/userdata/rkllm/py312/bin/python3.12-u/userdata/rkllm/assistant.py# 不开口直接喂一句话验证 LLM TTS 播放/userdata/rkllm/py312/bin/python3.12-u/userdata/rkllm/assistant.py\--text现在温度多少# 用录好的 wav 走完整链路不含唤醒/userdata/rkllm/py312/bin/python3.12-u/userdata/rkllm/assistant.py\--file/userdata/rkllm/asr_out/cmd_20261017_184654.wav# 回退到 CPU VITS/userdata/rkllm/py312/bin/python3.12-u/userdata/rkllm/assistant.py--ttsvits六、附带解决让数字「念得像中文」换完模型后又冒出一个新问题TTS 前端会把 ASCII 数字按英文音素化。现在是 2026-10-21 22:05 → twenty twenty-six, ten, twenty-one, twenty-two oh five时间和系统状态是最常问的这么念非常出戏。解决办法不是改 TTS而是在工具层就把数字写成汉字TTS 只负责念不负责理解_CN_DIGITS零一二三四五六七八九def_cn_year(y):return.join(_CN_DIGITS[int(d)]fordinstr(y))defcn_date(now):return%s年%s月%s日 星期%s%(_cn_year(now.year),_cn_under100(now.month),_cn_under100(now.day),一二三四五六日[now.weekday()])defcn_time(now):# 22:05 → 晚上十点零五分...另外三个函数负责其它场景函数用途示例cn_int整数内存、条数、算式3893 → 三千八百九十三cn_dec小数温度、负载45.2 → 四十五点二3.50 → 三点五cn_digits数字串逐位读IP192.168.0.103 → 一九二点一六八点零点一零三工具层改完后同一条链路实测查询TTS 实际播报现在几点现在是 二零二六年十月二十一日 星期三晚上十点二十二分看看温度温度 soc 五十点八 度bigcore 五十二点七 度npu 四十九点九 度内存还剩多少内存共 三千八百九十三 兆可用 二千三百四十 兆本机 IP本机 IP 一二七点零点零点一、一九二点一六八点零点一零三看看待办你有 一 条待办一 明天买牛奶20 分钟后提醒我吃药好的二十分钟后会提醒你吃药12 乘以 8 等于多少十二 乘以 八 等于 九十六再用 Paraformer 回读合成出来的 wav确认发音是中文而不是英文内存共三千八百九十三兆 可用二千三百四十兆 温度 soc 五十点八度 十二乘以八等于九十六首声仍是 0.42 ~ 0.43 sRTF 0.14 ~ 0.18 ——中文读法没有带来额外开销。七、踩坑清单坑现象解法matcha.tar.bz2是截断坏包14.7 MB解压报错真实约 79 MB重新完整下载解压后应有 76 MB 资源目录模型路径少一层models/后端静默不加载_matcha_backend is None没有任何警告默认路径写成models/matcha-s64.rknn尾部残响剪不掉阈值 0.01杂音 RMS 0.108100 ms 窗能量分段保留最后一个 ≥0.4 s 语音段关掉切句反而更慢首声 1.15 s音频估长到 11.41 s保持按句切分24/40libespeak-ng-libespeak12974 字节过渡包装上无效用libespeak-ng1dpkg-deb -x解包pkill -f assistant.py把自己的 SSH 会话一起杀了用ps -eo pid,args | grep ssistant | grep -v grep精确找 PID系统 Python 3.13装不了rknn-toolkit-lite2无 pip/apt/gccpython-build-standalone CPython 3.12 隔离环境Kokoro 看起来很美实测 RTF 3.6~4.0比 VITS 还慢任何模型都必须在目标板上实测八、总结与下一步VITS 慢的根因不是线程数而是非流式 CPU 算力不足。RTF 1.7 意味着永远追不上实时调参只能在 1.7~1.9 之间打转。切句流水线能把首声从 8~11 s 降到 2.5~3.5 s但 RTF 1长回答仍会出现块间空档只能救急。Kokoro 在 RK3576 上更慢原厂 RKNN TTS 受 VITS flow 算子限制 —— 「换 NPU」并不自动等于「变快」。Matcha条件流匹配 Vocos纯卷积声码器全部静态编译进 RKNN是这块板子上目前最优解RTF 0.10、首声 0.4 s、整机 1.2 s 出声。固定 shape 模型必须配trim_tail否则尾部会多出「静音 杂音」同时切句不能省首块越短首声越快。中文数字读法要在工具层做别指望 TTS 前端把22:05念成中文。所有版本要对齐librknnrt2.3.2 rknn-toolkit-lite22.3.2 模型编译时的 toolkit 2.3.2。下一步可以做的真正的流式 TTS目前 Matcha 是固定窗口整段出音一次吐出 4 秒要更低延迟需要能分块喂声码器的流式实现时长估计修正纯中文的 mel 帧数估计偏大 30%~40%现在靠trim_tail兜底后续可以按文本长度做线性校正减少无效计算多说话人 / 音色切换Vocos 本身与音色无关换 Matcha 说话人 embedding 即可。本文为 Quectel Pi M2 离线语音助手系列实践第四篇TTS 低延迟优化VITS / Kokoro / MatchaVocos 实测对比。系列前作板载麦克风与蓝牙播放 中文语音唤醒KWS RKLLM NPU 推理下一篇计划中ASRParaformer 意图工具层 整机闭环

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询