PaddleSpeech 流式 TTS 推理与实时播放:stream_play_tts 模块原理与实践
2026/9/24 14:43:16 网站建设 项目流程
  • 人工智能
  • 语音
  • 音频
  • NLP
  • 媒体生成

【免费下载链接】PaddleSpeech

Easy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.

项目地址:https://gitcode.com/paddlepaddle/PaddleSpeech
点击查看免费下载

本篇文章以 paddlespeech.t2s.exps.stream_play_tts 模块为主体,讲解 PaddleSpeech 中基于 ONNX Runtime 的流式语音合成(Streaming TTS)实现:如何在 CPU 上使用 fastspeech2_cnndecoder 流式声学模型与 mb_melgan 流式声码器,实现"边合成边播放"的实时体验。读完本文,你将掌握流式 AM/Vocoder 的分块(chunk)推理、去 padding(depadding)拼接、滑动窗口机制,以及如何用 pyaudio 实时播放并导出流式合成结果。

模块定位:把流式 TTS 跑成本地脚本

paddlespeech.t2s.exps.stream_play_tts是 PaddleSpeech 中一个独立的实验(experiment)级脚本模块,与基于服务端架构的 demos/streaming_tts_server 不同,它以单机脚本形式演示了流式 TTS 的完整链路:中文文本前端 → 流式 AM(声学模型)分块推理 → 流式 Vocoder(声码器)分块推理 → 实时声卡播放 → 保存 wav。

模块的核心结论是:不需要等整句话合成完毕,只要第一个 AM chunk 的 Mel 帧数达到 Vocoder 的 chunk 大小,就可以立刻开始播放第一批音频,从而把首包延迟压到极低。该模块对应的 API 文档为 docs/source/api/paddlespeech.t2s.exps.stream_play_tts.rst,其内容通过automodule指令直接引用本模块源码中的 docstring 与成员。

运行前的准备:模型文件与依赖

下载并解压模型

源码注释(stream_play_tts.py)要求首次运行前在执行目录下下载并解压两个 ONNX 模型包:

  • fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0.zip:流式 AM(声学模型),解压后包含phone_id_map.txtspeech_stats.npyfastspeech2_csmsc_am_encoder_infer.onnxfastspeech2_csmsc_am_decoder.onnxfastspeech2_csmsc_am_postnet.onnx等文件;
  • mb_melgan_csmsc_onnx_0.2.0.zip:流式声码器,解压后包含mb_melgan_csmsc.onnx

下载地址(wget 命令)见 stream_play_tts.py 文件头部注释,下载后使用unzip解压到当前目录即可。注意脚本中的路径均为相对路径,因此必须在解压目录下运行脚本

Python 依赖

脚本 import 了以下关键库,运行前需确保已安装:

依赖用途
onnxruntimeONNX 模型推理引擎(脚本以CPUExecutionProvider在 CPU 上运行)
pyaudio实时声卡播放
numpy张量运算与拼接
soundfile最终结果导出为 wav
paddle(经由 Frontend 间接使用)文本前端将文本转为 phone id

同时,脚本复用了 PaddleSpeech server 模块的工具函数:

  • paddlespeech.server.utils.util.denorm:流式 AM 输出的反归一化;
  • paddlespeech.server.utils.util.get_chunks:按 block/pad 切分 chunk;
  • paddlespeech.server.utils.audio_process.float2pcm:float32 转 int16 PCM;
  • paddlespeech.t2s.frontend.zh_frontend.Frontend:中文文本前端。

核心参数:block 与 pad 的物理含义

脚本顶部定义了一组流式推理的全局常量(stream_play_tts.py):

voc_block = 36 # 声码器每个 chunk 的有效 Mel 帧数 voc_pad = 14 # 声码器每个 chunk 前后各补的 Mel 帧数 am_block = 72 # 声学模型每个 chunk 的有效帧数 am_pad = 12 # 声学模型每个 chunk 前后各补的帧数 voc_upsample = 300 # 声码器上采样倍数(对应 voc 配置的 n_shift)

流式推理的基本矛盾是:模型在推理单个 chunk 时,chunk 边界处的帧会因为缺少上下文而产生误差。解决办法是每个 chunk 除了有效帧block,还要在前后各多取pad帧参与推理,推理完成后只保留中间有效部分,两侧 padding 结果被丢弃depadding()函数就是干这件事的:

  • 首块(chunk_id == 0):只取前block * upsample个采样点;
  • 尾块(chunk_id == chunk_num - 1):丢弃开头front_pad * upsample个采样点;
  • 中间块:取[front_pad * upsample, (front_pad + block) * upsample)区间。

这里的front_pad = min(chunk_id * block, pad),保证第一块不会取负索引。详细实现见 stream_play_tts.py。

这些参数的取值并非随意,它们与合成质量直接相关,在流式 TTS 服务配置 demos/streaming_tts_server/conf/tts_online_application.yaml 中有更完整的说明:

  • am_block: 72, am_pad: 12am_pad=12时流式 AM 合成音频与非流式完全一致(仅对 fastspeech2_cnndecoder 生效);
  • voc_block: 36, voc_pad: 14:mb_melgan 在voc_pad=14时流式与非流式一致,最低可设到 7(听感正常),低于 7 会出现异常听感;
  • voc_upsample: 300:必须与 vocoder 配置中的n_shift一致(tts_online_application.yaml),它决定了"1 帧 Mel 对应多少采样点",是 Mel 帧与波形采样点换算的关键。

模型加载与文本前端

加载 ONNX Session

脚本为 AM 的三个子模型和 vocoder 分别创建onnxruntime.InferenceSession(stream_play_tts.py),全部使用CPUExecutionProvider

  • am_encoder_infer:将 phone id 序列编码为 hidden state;
  • am_decoder:将 hidden state chunk 解码为 Mel;
  • am_postnet:对 Mel 做后处理增强(残差修正,最终输出为 decoder 输出 + postnet 输出);
  • voc_melgan:将 Mel chunk 上采样为波形。

之所以把 AM 拆成 encoder/decoder/postnet 三个 ONNX,正是因为只有 decoder 和 postnet 是逐 chunk 流式执行的,encoder 仍可一次性处理整句 phone id——这是 fastspeech2_cnndecoder 支持流式 AM 的结构基础。

文本前端

使用 Frontend(paddlespeech.t2s.frontend.zh_frontend)将中文文本转为 phone id:

phones_dict = "fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0/phone_id_map.txt" frontend = Frontend(phone_vocab_path=phones_dict, tone_vocab_path=None)

inference_stream()中调用frontend.get_input_ids(text, merge_sentences=False, get_tone_ids=False)获得phone_ids(stream_play_tts.py)。get_input_ids会先经过get_phonemes做中文分词、注音与韵律处理,再把音素映射为词典 id;merge_sentences=False表示按句切分、逐句返回,脚本外层用for i in range(len(phone_ids))遍历每一句(get_input_ids)。由于输入是 numpy 数组,该路径可以无缝喂给 onnxruntime session。

流式 AM 推理:chunk 级 Mel 生成

流式 AM 的流程(inference_stream)如下:

  1. 整句编码:将一句 phone id 送入am_encoder_infer_sess,得到整句的 hidden stateorig_hs,其 shape 为[1, mel_len, dim]
  2. 切 chunk:调用get_chunks(orig_hs, am_block, am_pad, "am")切出am_chunk_num个 chunk。get_chunks(util.py)中step="am"时沿第 1 维(时间帧维度)切分,每块为[1, block+2*pad, dim]
  3. 逐 chunk 推理:每个 chunkhs依次送入am_decoder_sess得到 Mel,再转置为[B, C, T]送入am_postnet_sess,最终 Mel 为decoder 输出 + postnet 输出(残差结构,stream_play_tts.py);
  4. 反归一化:因为流式 AM 模型训练时对 Mel 做了均值方差归一化,推理后需用speech_stats.npy中的am_muam_stddata * std + mean还原(denorm,由np.load(am_stat_path)加载统计量);
  5. 去 padding:对反归一化后的 chunk 调用depadding(..., upsample=1)丢弃两侧 pad 帧(Mel 维度上 upsample 为 1),并np.concatenate拼接成mel_streaming

经过以上步骤,mel_streaming就是"到目前为止已经高质量合成出的 Mel 帧序列",它会被喂给下一阶段的流式 vocoder。

流式 Vocoder 推理:滑动窗口与实时播放

流式 vocoder 的核心是双指针滑动窗口(stream_play_tts.py):

mel_len = orig_hs.shape[1] voc_chunk_num = math.ceil(mel_len / voc_block) start = 0 end = min(voc_block + voc_pad, mel_len)

当流式 AM 累积的mel_streaming.shape[0] >= end时(Mel 帧足够一次 vocoder 推理),立即取mel_streaming[start:end, :]送入voc_melgan_sess得到波形 chunk,经depadding(..., upsample=voc_upsample)去掉 padding 采样点后用yield抛出。随后窗口滑动:

voc_chunk_id += 1 start = max(0, voc_chunk_id * voc_block - voc_pad) end = min((voc_chunk_id + 1) * voc_block + voc_pad, mel_len)

start回退voc_pad帧保证 chunk 间上下文衔接,endmin限制在mel_len内。因为inference_stream是生成器函数,yield一个波形 chunk,主循环就可以立刻播放一段音频,实现真正的流式播放,这正是"首包延迟低"的原因。

主流程:Warmup、实时播放与导出

__main__中的运行逻辑(stream_play_tts.py)分为三步:

  1. Warmup:onnxruntime 首次推理开销较大(session 初始化、算子预热),先用"哈哈哈哈"空转一遍全部 session,避免把首次推理耗时算进首包延迟(stream_play_tts.py)。这与流式 TTS 服务启动时打印的 warm up 日志(见 demos/streaming_tts_server/README.md)是同一设计思路;
  2. pyaudio 实时播放:以int16、单声道、24000 Hz打开输出流(与 AM/Vocoder 的采样率一致)。对每个sub_wavchunk,先用 float2pcm 把 [-1, 1] 的 float32 转为 int16 PCM(sig * abs_max + offset缩放并 clip),再tobytes()stream.write()写入声卡,实现边合成边播放。每收到一个 chunk 打印一次响应耗时;
  3. 导出 wav:把各 chunkflatten()np.concatenate拼成完整波形,用soundfile以 24000 Hz 写入demo_stream.wav(stream_play_tts.py)。

运行方式:

python paddlespeech/t2s/exps/stream_play_tts.py

运行时会在控制台逐 chunk 打印"响应时间",最终在脚本目录下生成demo_stream.wav,同时扬声器实时播放合成语音。

与流式 TTS 服务的参数对应关系

本脚本采用的参数(72/12/36/14/300)正是流式 TTS 服务中tts_online-onnx引擎的默认配置(tts_online_application.yaml)。二者共享同一套流式推理约束:

  • 支持流式 AM 的模型只有fastspeech2_cnndecoder系列(非流式fastspeech2不支持 AM 分块,am_block/am_pad无效);
  • 支持流式 vocoder 的有mb_melganhifigan;其中 hifigan 需要更大的 pad(voc_pad=19时与非流式一致,14时听感正常);
  • voc_upsample必须与 vocoder 配置的n_shift一致,否则波形时长会错误;
  • 推理速度 mb_melgan > hifigan,音频质量反之(demos/streaming_tts_server/README.md)。

因此,理解本脚本就等于理解了 PaddleSpeech 流式 TTS 服务的核心推理内核:服务端只是在生成器之上封装了 HTTP/WebSocket 传输协议与客户端播放,而stream_play_tts.py用最少的代码把这条流式链路完整、可运行地呈现在读者面前。

总结

paddlespeech.t2s.exps.stream_play_tts是学习 PaddleSpeech 流式 TTS 原理的最佳入口。它以约 180 行代码串联起文本前端、流式 AM(encoder/decoder/postnet 三子模型 +get_chunks/denorm/depadding)、流式 vocoder(双指针滑动窗口)与实时播放(pyaudio)四大环节。核心可复用的经验包括:用pad帧消除分块边界误差、用生成器 +yield实现逐 chunk 输出、用 warmup 规避 onnxruntime 首次推理开销。若需在生产环境使用同等能力,可参考 demos/streaming_tts_server 部署为服务化形态。

  • 人工智能
  • 语音
  • 音频
  • NLP
  • 媒体生成

【免费下载链接】PaddleSpeech

Easy-to-use Speech Toolkit including Self-Supervised Learning model, SOTA/Streaming ASR with punctuation, Streaming TTS with text frontend, Speaker Verification System, End-to-End Speech Translation and Keyword Spotting. Won NAACL2022 Best Demo Award.

项目地址:https://gitcode.com/paddlepaddle/PaddleSpeech
点击查看免费下载

相关推荐

上一篇:alpaca.cpp性能基准:行业标准测试集跑分结果
下一篇:RegExr产品路线图制定:用户需求与技术可行性平衡

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询