PaddleSpeech 流式 TTS 实战:基于 ONNX Runtime 的 stream_play_tts 模块源码解析与边合成边播放实现
【免费下载链接】PaddleSpeechEasy-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/gh_mirrors/pa/PaddleSpeech
本篇文章以 PaddleSpeech 仓库中paddlespeech.t2s.exps.stream_play_tts模块(对应 docs/source/api/paddlespeech.t2s.exps.stream_play_tts.rst 文档条目)为主体,深入讲解其底层实现 stream_play_tts.py 的完整运行机制:从模型下载、前端文本转音素、流式声学模型(FastSpeech2 CNNDecoder)与流式声码器(MB-MelGAN)的 ONNX 分块推理,到去重叠(depadding)、PyAudio 实时播放与 WAV 导出。读完本文,你将掌握 PaddleSpeech 端到端流式语音合成的最小可运行实现,理解am_block / am_pad / voc_block / voc_pad等核心参数的意义与取值依据,并能基于源码自行复现"文本输入 → 首包语音快速响应 → 边合成边播放"的完整链路。
模块定位:API 文档背后的流式 TTS 播放脚本
在 docs/source/api/paddlespeech.t2s.exps.stream_play_tts.rst 中,该模块以 Sphinxautomodule指令自动提取 docstring 的方式生成 API 文档,被收录于paddlespeech.t2s.exps包(见 paddlespeech.t2s.exps.rst),与ort_predict、ort_predict_streaming、synthesize_streaming等推理脚本并列。它对应的唯一源码文件是 paddlespeech/t2s/exps/stream_play_tts.py,属于 TTS 实验(exps)目录下的一个独立可执行脚本。
该脚本的核心目标是演示流式语音合成 + 实时播放:
- 使用 ONNX Runtime 加载流式声学模型(FastSpeech2 CNNDecoder)与流式声码器(MB-MelGAN);
- 将文本经中文前端(paddlespeech/t2s/frontend/zh_frontend.py)转换为音素 ID;
- 声学模型按块(chunk)推理出归一化梅尔谱,声码器按块合成语音片段;
- 通过 PyAudio 边合成边播放,实现类似"流式输出"的用户体验;
- 同时把所有片段拼接导出为
demo_stream.wav文件。
从源码结构看,这个脚本是"流式推理 + 播放"的最小可运行样例,它复用了paddlespeech.server.utils中的通用工具函数,可作为独立脚本直接运行,也是理解demos/streaming_tts_server服务端流式 TTS 原理的极佳起点。
运行前准备:依赖安装与模型下载
脚本顶部注释明确要求"首次执行前,先在执行目录下载并解压模型",涉及两个模型包:
wget https://paddlespeech.cdn.bcebos.com/Parakeet/released_models/fastspeech2/fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0.zip wget https://paddlespeech.cdn.bcebos.com/Parakeet/released_models/mb_melgan/mb_melgan_csmsc_onnx_0.2.0.zip unzip fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0.zip unzip mb_melgan_csmsc_onnx_0.2.0.zip两个模型包解压后分别包含:
| 文件 | 作用 |
|---|---|
fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0/phone_id_map.txt | 音素到 ID 的映射表,供前端 Frontend 加载 |
fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0/speech_stats.npy | 梅尔谱归一化统计量(均值am_mu、标准差am_std) |
fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0/fastspeech2_csmsc_am_encoder_infer.onnx | 声学模型编码器(文本 → hidden state) |
fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0/fastspeech2_csmsc_am_decoder.onnx | 声学模型解码器(hidden state → 归一化梅尔谱) |
fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0/fastspeech2_csmsc_am_postnet.onnx | 声学模型 Postnet 后处理网络 |
mb_melgan_csmsc_onnx_0.2.0/mb_melgan_csmsc.onnx | MB-MelGAN 流式声码器(梅尔谱 → 波形) |
脚本依赖的外部 Python 库包括numpy、onnxruntime(ONNX 推理)、pyaudio(实时音频播放)、soundfile(WAV 落盘),以及仓库内的paddlespeech.server.utils.audio_process、paddlespeech.server.utils.util与paddlespeech.t2s.frontend.zh_frontend模块。
核心超参数:块大小(block)与填充(pad)
流式合成的关键是把长序列切块、逐块推理并拼回,而块与块之间的上下文衔接依赖"填充(pad)"机制。脚本在 stream_play_tts.py 顶部定义了四个核心参数:
voc_block = 36 # 声码器每次推理的梅尔帧数 voc_pad = 14 # 声码器每块前后填充的梅尔帧数 am_block = 72 # 声学模型每次推理的帧数 am_pad = 12 # 声学模型每块前后填充的帧数 voc_upsample = 300 # MB-MelGAN 的上采样倍数(每帧梅尔谱对应 300 个采样点)这些参数与流式 TTS 服务端配置完全一致。在 demos/streaming_tts_server/conf/tts_online_application.yaml 中,tts_online引擎(engine_type: online)与tts_online-onnx引擎(engine_type: online-onnx)均配置了相同取值,并且配置文件给出了权威注释,可作为参数选择依据:
am_block: 72、am_pad: 12仅用于fastspeech2_cnndecoder系列模型做流式声学推理;当am_pad设为 12 时,流式合成的音频与非流式合成完全一致;voc_block: 36、voc_pad: 14用于流式声码器推理;当声码器为mb_melgan_csmsc时,voc_pad设为 14 可与非流式结果一致,最小值可降至 7 而听感正常;- 当声码器换为
hifigan_csmsc时,voc_pad需设为 19 才能与非流式一致,设为 14 则听感正常。
这组参数直观体现了流式合成的"质量-延迟"权衡:pad 越大,块与块之间的上下文越完整、合成质量越接近非流式,但首包等待与计算量也会相应增加。
前端与模型加载:从文本到音素、从 ONNX 文件到 Session
中文前端 Frontend
脚本使用Frontend(phone_vocab_path=phones_dict, tone_vocab_path=None)构建中文前端,其中phone_vocab_path指向模型包内的phone_id_map.txt,tone_vocab_path传None表示该流式模型不区分声调。前端类定义于 paddlespeech/t2s/frontend/zh_frontend.py,其get_input_ids方法(见同文件 L645-L697)接收merge_sentences、get_tone_ids、robot、add_blank等参数,内部先做文本正则化与字音转换得到音素序列,再查表映射为phone_ids/tone_ids张量,返回形如{"phone_ids": [Tensor, ...]}的字典。
脚本中的调用方式:
input_ids = frontend.get_input_ids( text, merge_sentences=False, get_tone_ids=False) phone_ids = input_ids["phone_ids"]merge_sentences=False表示按句子切分(不合并),get_tone_ids=False表示不输出声调 ID——与前端初始化时tone_vocab_path=None的设置保持一致,最终取phone_ids作为声学模型编码器的输入。
归一化统计量
am_stat_path = "fastspeech2_cnndecoder_csmsc_streaming_onnx_1.0.0/speech_stats.npy" am_mu, am_std = np.load(am_stat_path)speech_stats.npy保存训练时梅尔谱的均值与标准差,用于推理后将模型输出的归一化梅尔谱反归一化还原为真实梅尔谱(对应下文denorm步骤)。
创建 ONNX Runtime Session
脚本显式指定providers = ['CPUExecutionProvider']进行 CPU 推理,并统一使用默认SessionOptions创建四个 Session:
am_encoder_infer_sess = ort.InferenceSession(onnx_am_encoder, providers=providers, sess_options=sess_options) am_decoder_sess = ort.InferenceSession(onnx_am_decoder, providers=providers, sess_options=sess_options) am_postnet_sess = ort.InferenceSession(onnx_am_postnet, providers=providers, sess_options=sess_options) voc_melgan_sess = ort.InferenceSession(onnx_voc_melgan, providers=providers, sess_options=sess_options)从源码结构看,FastSpeech2 CNNDecoder 被拆分为 Encoder / Decoder / Postnet 三个 ONNX 子模型,分别负责"文本 → hidden state"、"hidden state → 归一化梅尔谱"与"梅尔谱精修",三者串行调用构成完整的声学模型;MB-MelGAN 则作为单独的声码器模型。
流式推理核心逻辑:inference_stream 生成器
inference_stream(text)是一个 Python 生成器函数,通过yield逐段输出语音波形,这是"边合成边播放"的关键——主程序可以每拿到一个sub_wav立即写入音频设备,而无需等待整段合成完毕。其完整流水线如下:
- 文本 → 音素 ID:调用
frontend.get_input_ids得到每句话的phone_ids; - AM Encoder 整句推理:将整句音素 ID 一次送入
am_encoder_infer_sess,得到完整 hidden stateorig_hs(形状为[1, mel_len, hidden]); - 规划声码器块:根据梅尔帧总数
mel_len计算声码器分块数voc_chunk_num = ceil(mel_len / voc_block),并初始化当前块的start/end; - AM 流式分块:调用
get_chunks(orig_hs, am_block, am_pad, "am")把 hidden state 切成带 pad 的块,逐块送入 Decoder 与 Postnet,相加得到归一化梅尔谱,再经denorm反归一化; - AM 去重叠:对每个 AM 块调用
depadding(...)去掉因 pad 引入的重叠帧,拼接为完整(或当前已合成部分)的梅尔谱mel_streaming; - 声码器流式触发:当
mel_streaming的帧数满足mel_streaming.shape[0] >= end且未达到voc_chunk_num时,取mel_streaming[start:end, :]送入voc_melgan_sess,对输出波形再执行一次depadding,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)更新窗口。
通过上述"AM 整句编码 + 分块解码 + 声码器按需消费"的流水线,Mel 谱一旦累积到声码器所需的块长度,即可立刻合成并输出一段波形,从而在整句文本尚未全部解码完成前就开始播放语音。
去重叠函数 depadding 详解
分块推理时,每个块都会在首尾多算pad帧以保证上下文连续,因此拼接前必须把重叠区域去掉。depadding 按块位置分三种情况处理:
def depadding(data, chunk_num, chunk_id, block, pad, upsample): front_pad = min(chunk_id * block, pad) # 第一块:只保留前 block 帧 if chunk_id == 0: data = data[:block * upsample] # 最后一块:去掉前面累积的填充帧 elif chunk_id == chunk_num - 1: data = data[front_pad * upsample:] # 中间块:去掉首尾填充,只保留中间 block 帧 else: data = data[front_pad * upsample:(front_pad + block) * upsample] return data要点在于:第chunk_id块的"有效内容"位于该块[chunk_id * block, (chunk_id + 1) * block)区间,块首的重叠量等于min(chunk_id * block, pad)(前几块不足 pad 时重叠量更小);upsample参数把帧维度换算为采样点维度——AM 阶段去重叠时upsample=1(处理梅尔帧),声码器阶段去重叠时upsample=voc_upsample=300(处理波形采样点)。首块特殊处理是因为没有前文填充,直接截取前block * upsample即可。
声学模型分块:get_chunks 工具函数
AM 分块调用的是仓库通用工具 paddlespeech/server/utils/util.py 中的get_chunks(data, block_size, pad_size, step):
def get_chunks(data, block_size, pad_size, step): if block_size == -1: return [data] if step == "am": data_len = data.shape[1] # 对 hidden state,取时间维 elif step == "voc": data_len = data.shape[0] chunks = [] n = math.ceil(data_len / block_size) for i in range(n): start = max(0, i * block_size - pad_size) end = min((i + 1) * block_size + pad_size, data_len) if step == "am": chunks.append(data[:, start:end, :]) elif step == "voc": chunks.append(data[start:end, :]) return chunks该函数按block_size等分数据,并在每个块前后各扩展pad_size帧(首尾用max/min收窄),step参数决定沿哪个维度切分以及返回的形状:AM 阶段传入的三维张量按中间时间维切成[1, block+2*pad, hidden],Voc 阶段按第一维切成[block+2*pad, feat]。它同时也是流式 TTS 服务端分块的基础设施,体现了"分块-填充-去重叠"这一流式推理通用范式的完整闭环。
主程序:warmup、实时播放、计时与导出
脚本的__main__入口(stream_play_tts.py)演示了完整的端到端使用流程:
1. 预热(Warmup)
for sub_wav in inference_stream(text="哈哈哈哈"): continue源码注释明确说明"onnxruntime 第一次时间会长一些,建议先 warmup 一下"。预热的意义在于让 ONNX Runtime 完成算子初始化与线程池预热,避免首次推理耗时污染后续的响应时间统计。
2. PyAudio 实时播放
p = pyaudio.PyAudio() stream = p.open( format=p.get_format_from_width(2), # int16 channels=1, rate=24000, output=True)播放参数为单声道、采样率 24000 Hz、16-bit PCM。注意rate=24000与voc_upsample = 300的内在关系:MB-MelGAN 每帧梅尔谱对应 300 个采样点,若其输入梅尔谱帧率(hop)为 80 Hz,则300 × 80 = 24000 Hz,与播放采样率完全吻合——这是声码器输出能直接送声卡播放的前提。
3. 逐块播放与计时
t1 = time.time() for sub_wav in inference_stream(text): print("响应时间:", time.time() - t1) t1 = time.time() wavs.append(sub_wav.flatten()) wav = float2pcm(sub_wav) # float32 → int16 wav_bytes = wav.tobytes() stream.write(wav_bytes) # 立即写入音频设备每拿到一个语音块,就打印该块的响应时间、通过float2pcm转为 16-bit PCM 并立即写入声卡,实现"边合成边播放"。float2pcm定义于 paddlespeech/server/utils/audio_process.py:它要求输入为浮点数组,先校验 dtype,再按目标整数位深缩放并 clip 到有效范围(默认目标类型int16),返回值可直接tobytes()交给音频设备。
4. 结果导出
stream.stop_stream(); stream.close(); p.terminate() wav = np.concatenate(wavs) print(wav.shape) sf.write("demo_stream.wav", data=wav, samplerate=24000)播放结束后,将所有sub_wav拼接为完整波形,用soundfile以 24000 Hz 采样率写为demo_stream.wav,方便离线核对流式合成结果与整句合成是否一致(配合前述am_pad=12、voc_pad=14的质量配置)。
归一化与反归一化:denorm 的桥梁作用
流式声学模型输出的是归一化梅尔谱(训练时以(mel - mu) / std归一化),因此推理后必须反归一化才能交给声码器。脚本调用仓库工具 paddlespeech/server/utils/util.py 中的denorm:
def denorm(data, mean, std): return data * std + mean即mel = normalized_mel * std + mean,其中mean/std来自前文加载的speech_stats.npy。该函数同样被流式 TTS 服务端引擎复用(见paddlespeech/server/engine/tts/相关实现),是"模型输出 → 可听波形"之间不可缺少的一环。
与其他模块的关联:从单机脚本到流式服务
stream_play_tts.py并非孤立存在,它与仓库内的流式 TTS 能力一脉相承:
- paddlespeech/t2s/exps/ort_predict_streaming.py 提供了面向文件输出的 ONNX 流式推理脚本(不含播放),
stream_play_tts可视为其"实时播放版"; - 服务端流式 TTS 的配置 demos/streaming_tts_server/conf/tts_online_application.yaml 与 tts_online_ws_application.yaml 使用了完全相同的
am_block / am_pad / voc_block / voc_pad参数体系,脚本中写死的四个常量即是这些配置项的脚本内等价物; - 分块工具
get_chunks、反归一化denorm、浮点转 PCM 的float2pcm均来自 paddlespeech/server/utils,说明该脚本与paddlespeech.server服务端共享同一套流式推理基础设施。
因此,理解stream_play_tts.py就等于掌握了 PaddleSpeech 流式 TTS 从模型推理到音频输出的最小核心链路,无论是将其改造为 WebSocket 实时播放、嵌入式 TTS,还是在此基础上做首包延迟优化,都能以此为起点。
小结:流式 TTS 播放脚本的关键要点
- 架构拆分:FastSpeech2 CNNDecoder 以 Encoder / Decoder / Postnet 三个 ONNX 子模型串行推理,MB-MelGAN 作为流式声码器独立消费梅尔谱块;
- 参数即质量开关:
am_pad=12、voc_pad=14可使流式结果与非流式一致,voc_pad最小可降至 7 以换听感可接受的低延迟; - 分块范式:
get_chunks负责带 pad 切块,depadding负责去除重叠区,AM 阶段upsample=1、声码器阶段upsample=300,二者构成完整的流式拼接闭环; - 实时体验:生成器 + PyAudio 边合成边播放,24000 Hz 采样率与声码器上采样倍数严格对应;首次推理建议 warmup 以消除初始化开销;
- 落盘验证:所有音频块拼接后以 24000 Hz 写入
demo_stream.wav,便于质量对比与结果复现。
如果需要在此基础上构建真正的流式 TTS 服务,可直接参考 demos/streaming_tts_server 的在线/在线 ONNX 两种引擎配置,将本脚本中的块参数、去重叠逻辑与播放链路迁移到服务端实现。
【免费下载链接】PaddleSpeechEasy-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/gh_mirrors/pa/PaddleSpeech
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考