如果你已经会用 CLI 和 Web Demo,下一步就应该开始看源码了。
这篇教程不追求把所有函数逐行讲完,而是给你一条更高效的阅读路线,尤其是:
- CLI 是怎么分发的
- Web Demo 的调用链是怎样的
- 实时流式为什么很难做到完全连续
1. 先看 CLI,总线在哪里
CLI 入口在:
这个文件的作用可以概括成一句话:
把用户输入的命令行参数,转发给真正干活的 infer.py / app.py / infer_onnx.py / app_onnx.py
建议优先看这些函数:
_build_parser_run_generate_pytorch_run_generate_onnx_run_serve_pytorch_run_serve_onnx
理解完这几个函数,你就会清楚:
generate不是独立系统serve也不是独立系统- 它们只是同一套能力的不同调用方式
2. generate 的调用链
当你执行:
bash
python -m moss_tts_nano generate ...
PyTorch 路线大致是:
text
moss_tts_nano/__main__.py
-> moss_tts_nano/cli.py
-> infer.py
-> model.inference(...)
如果是 ONNX 路线,则会走:
text
moss_tts_nano/__main__.py
-> moss_tts_nano/cli.py
-> infer_onnx.py
-> OnnxTtsRuntime.synthesize(...)
3. serve 的调用链
当你执行:
bash
python -m moss_tts_nano serve ...
PyTorch 路线大致是:
text
moss_tts_nano/__main__.py
-> moss_tts_nano/cli.py
-> app.py
-> FastAPI
-> runtime.synthesize(...) 或 runtime.synthesize_stream(...)
这个链路是理解 Web Demo 的关键。
为什么说 serve 只是"包装"
因为它本身并没有发明新的 TTS 能力,它只是:
- 启动服务
- 接 HTTP 请求
- 调已有 runtime
- 返回结果给浏览器
4. 先看 app.py 的哪几块
app.py 很长,不建议从头一行一行读。
建议按这个顺序看:
- 页面 HTML 模板和前端脚本
/api/generate/api/generate-stream/start_run_streaming_job(...)RequestRuntimeManager
你会发现它其实可以拆成三层:
第一层:页面层
负责:
- 展示输入项
- 收集参数
- 向后端发请求
- 播放音频
第二层:接口层
负责:
- 接收表单请求
- 做参数整理
- 调用 runtime
第三层:运行时层
负责:
- 真正跑模型
- 处理设备选择
- 处理流式推理
5. 为什么这次要改 Execution Device
在最初的实现里,Web Demo 的推理请求是写死到 CPU 的。
这会导致一个现象:
- 命令行
generate --device cuda可以用 GPU - 但网页
serve却没真正用上 GPU
后来在本仓库当前版本里,app.py 已经加上了:
- 页面里的
Execution Device - 后端
execution_device参数透传 --device cuda的启动支持
所以当前这套代码里,网页和 CLI 已经能对齐到同样的设备选择逻辑。
6. 实时流式的关键路径
实时流式要重点看这条链:
text
/api/generate-stream/start
-> 后台线程 _run_streaming_job(...)
-> runtime.synthesize_stream(...)
-> 生成音频块
-> 放入 audio_queue
-> 前端 fetch 音频流
-> 浏览器播放
表面上看很顺,但真正难的地方就在于:
每一小块音频什么时候产出、什么时候传输、什么时候播放
这三个时间轴必须尽量稳定,听感才会顺。
7. 我们这次调试到底排查了什么
这次真实调试基本沿着下面这条路径排查了一遍:
- 先确认是不是 Web Demo 根本没用上 GPU
- 再确认是不是环境错误导致实际跑到系统 Python
- 再确认是不是
torch装成了 CPU 版 - 再调 Web 参数:
Initial Playback DelayVoice Clone Max Text TokensMax TTS Batch SizeMax Codec Batch Size
- 再尝试前端播放聚合
- 再尝试服务端 PCM 聚合
得到的结论很有价值:
- GPU 确实有帮助
- 调大 batch 也确实有帮助
- 但帮助有限
- 前端和服务端简单聚合都没有带来决定性变化
这说明问题更像是:
model.inference_stream(...)的输出节奏- codec streaming decode 的行为
- 以及整条实时链路的天然抖动
8. 为什么实时流式还是会有一点点不连续
这是这次学习里最重要的工程认知之一。
你不能把实时模式理解成:
GPU 够强 -> 一定丝滑
真实情况是:
- GPU 只解决"算得快不快"
- 但实时播放还要求"供给节奏稳不稳"
而节奏稳定性受到很多因素影响:
- 模型每次吐 token 的时间抖动
- codec 解码的抖动
- 服务端线程调度
- 浏览器
AudioContext的调度粒度
所以,哪怕:
- 显存还没满
- batch 调大了
- 前端缓冲也加了
仍然可能存在轻微不连续。
9. 如果你还想继续深挖,下一步该看什么
如果你只是想学项目,到这里已经够用了。
如果你还想继续深挖实时流式,下一步建议直接看:
优先关注这些内容:
synthesize(...)synthesize_stream(...)split_voice_clone_text(...)- CUDA 相关的 stream decode budget patch
你要重点观察的是:
- 每个
audio事件吐出的 waveform 有多长 is_pause在什么情况下出现lead_seconds是怎么变化的- 为什么流式 decode 预算会影响听感
10. 学习建议
如果你准备继续读这个项目源码,推荐方式是:
- 先画调用链
- 再看参数怎么传递
- 最后才看底层实现细节
具体到这个项目:
- 先看 moss_tts_nano/cli.py
- 再看 app.py
- 再看 infer.py
- 最后看 moss_tts_nano_runtime.py
这样会比一上来读 runtime 轻松很多。
11. 现在可以怎么用这个项目
基于这次调试,一个更务实的使用建议是:
学习和体验
优先用 serve
因为它能同时看到:
- 参数
- 接口
- 实时播放
- 整体调用链
真正听质量
优先关闭实时流式
因为一次性生成的结果通常更稳定。
做实时演示
可以用实时流式,但要接受一个现实:
- 它可以很好地展示"边生成边播"
- 但不一定能像离线完整播放那样绝对连续
小结
学 MOSS-TTS-Nano 时,最重要的不是死记参数,而是建立一套分层理解:
- CLI 负责分发
infer/app负责入口- runtime 负责真正推理
- 实时流式难点在整条链路的节奏稳定性