ChatTTS API 服务部署实战:FastAPI 原生接口与 OpenAI 兼容 TTS 网关
本文基于仓库中 examples/api 的官方 API 示例文档展开,讲解如何用 FastAPI 把 ChatTTS 部署为可远程调用的语音合成服务:既包括原生 /generate_voice 接口的完整参数体系与客户端调用方式,也包括一套兼容 OpenAI TTS 接口规范的 /v1/audio/speech 网关。读完本文,你可以从零启动两种服务、构造合法请求体、控制音色与韵律参数,并理解每个 HTTP 参数在 ChatTTS/core.py 推理管线中的实际落点。
服务架构与文件布局
examples/api 目录下的官方文档把整套服务拆成了“两个服务端 + 两个客户端”的最小集合:
| 文件 | 角色 | 说明 |
|---|---|---|
| main.py | 原生服务端 | 暴露 POST /generate_voice,透传 ChatTTS 全部推理参数,返回 mp3 压缩包 |
| openai_api.py | OpenAI 兼容服务端 | 暴露 POST /v1/audio/speech 与 GET /health,请求字段对齐 OpenAI TTS 规范 |
| client.py | 原生客户端 | 用 requests 直接请求 /generate_voice,解压结果到 output 目录 |
| postScript.py | 命令行客户端 | 通过 argparse 暴露全部参数,适合脚本化调用 |
| requirements.txt | 依赖清单 | 仅 fastapi 与 requests 两项 |
两个服务端共享同一套底座:启动时实例化 ChatTTS.Chat,注册中英文文本归一化器,再从 HuggingFace 拉取模型。区别在于参数暴露方式——main.py 把底层数据类参数原样交给调用方,而 openai_api.py 把参数收敛为 OpenAI 风格的白名单字段,内部写死一套经过调优的采样配置。
环境准备
官方文档给出的安装步骤是:
pip install -r examples/api/requirements.txt
其中 examples/api/requirements.txt 只包含 fastapi 和 requests。但要让服务端真正跑起来,还需要仓库根依赖 requirements.txt,其核心包括:
torch>=2.1.0、torchaudio、numpy<3.0.0:推理运行时;transformers>=4.41.1、vocos、vector_quantize_pytorch:Vocos 声码器与 DVAE 重建所需;pynini==2.1.5、WeTextProcessing、nemo_text_processing:仅 Linux 平台(sys_platform == 'linux')安装,分别支撑中文与英文文本归一化。
这里有一个容易踩坑的点:main.py 与 openai_api.py 的启动事件里都显式注册了两个归一化器:
chat.normalizer.register("en", normalizer_en_nemo_text()) # tools/normalizer/en.py
chat.normalizer.register("zh", normalizer_zh_tn()) # tools/normalizer/zh.py
对应源码位于 tools/normalizer/en.py 与 tools/normalizer/zh.py。若在 Linux 上未安装 nemo_text_processing / WeTextProcessing,而请求又开启 do_text_normalization=true(默认开启),文本预处理会不可用。因此建议先安装根依赖再启动服务。
模型本身不需要手动下载:两个服务端均调用 chat.load(source="huggingface"),从源码 ChatTTS/core.py 的 download_models 可以看到,首次运行会通过 snapshot_download 拉取 2Noise/ChatTTS 仓库中的 *.yaml、*.json、*.safetensors 资产,并校验 res/sha256_map.json。注意模型权重以 CC BY-NC 4.0 发布,仓库 README 明确其面向教育与研究用途(见 README.md)。
启动原生服务 main.py
官方文档给出的启动命令:
fastapi dev examples/api/main.py --host 0.0.0.0 --port 8000
--host 0.0.0.0 使服务监听所有网卡,便于容器或局域网内调用。启动阶段 main.py 完成了三件事:
- 实例化
ChatTTS.Chat并注册中英文归一化器; chat.load(source="huggingface")下载并加载 vocos、dvae、embed、gpt、speaker、decoder、tokenizer 等模块,加载失败直接sys.exit(1);- 另外还设置了平台兼容开关——macOS 下导出
PYTORCH_ENABLE_MPS_FALLBACK=1(main.py),因为源码中 Vocos 在 MPS 设备上会回退到 CPU 执行(core.py 有对应注释)。
接口与请求模型
服务只暴露一个端点 POST /generate_voice,请求体由 Pydantic 模型 ChatTTSParams 定义,字段与 Chat.infer 的签名一一对应:
| 字段 | 类型 / 默认值 | 说明 |
|---|---|---|
text |
list[str],必填 |
待合成文本列表,每项通常是一句 |
stream |
bool = False |
是否流式;为 True 时 chat.infer 返回生成器,服务端会把每个流式块分别编码为 0.mp3、1.mp3… 打进 zip |
lang |
str = None |
文本语言,用于选择归一化路径 |
skip_refine_text |
bool = False |
跳过文本精修(text refining),可显著降低延迟 |
refine_text_only |
bool = False |
只返回精修后的文本、不合成音频 |
use_decoder |
bool = True |
True 走独立 decoder 输出 mel 特征,False 走 dvae |
do_text_normalization |
bool = True |
执行文本归一化(数字、符号读法等) |
do_homophone_replacement |
bool = False |
基于 res/homophones_map.json 做同音字替换 |
params_refine_text |
RefineTextParams |
文本精修采样参数,结构体见下文 |
params_infer_code |
InferCodeParams,必填 |
声学码本生成参数,含 spk_emb 音色控制 |
服务端对请求还做了两层“种子”处理(main.py):
- 若
params_infer_code.manual_seed非空,先调用torch.manual_seed(...)固定随机状态,并通过chat.sample_random_speaker()采样一个说话人嵌入; - 若传入了
params_refine_text,会先以refine_text_only=True跑一次文本精修,把结果作为正式合成的输入文本。
响应是 StreamingResponse,媒体类型为 application/zip:服务端把所有句子的 PCM 数组经 pcm_arr_to_mp3_view 转为 mp3 字节流,逐条写入内存 zip(命名 {idx}.mp3),再一次性下发(main.py)。请求体校验失败时,自定义异常处理器统一返回 422 与字段级错误明细(main.py)。
用 client.py 发起请求
官方文档的最后一步:
python examples/api/client.py
client.py 的行为:
- 从环境变量
CHATTTS_SERVICE_HOST/CHATTTS_SERVICE_PORT读取服务地址(默认localhost:8000),拼出/generate_voiceURL; - POST 一个示例请求体(中文双句文本),其中关键默认值:
stream=False、skip_refine_text=True、do_text_normalization=True; - 把返回的 zip 按时间戳解压到
./output/{timestamp}/。
官方文档说明“mp3 音频文件会保存到 output 目录”,对应的就是这一步。完整请求体结构如下,可复制到任何 HTTP 客户端使用:
{
"text": ["第一句话", "第二句话"],
"stream": false,
"lang": null,
"skip_refine_text": true,
"refine_text_only": false,
"use_decoder": true,
"do_text_normalization": true,
"do_homophone_replacement": false,
"params_refine_text": {
"prompt": "", "top_P": 0.7, "top_K": 20, "temperature": 0.7,
"repetition_penalty": 1, "max_new_token": 384, "min_new_token": 0,
"show_tqdm": true, "ensure_non_empty": true, "stream_batch": 24
},
"params_infer_code": {
"prompt": "[speed_5]", "top_P": 0.1, "top_K": 20, "temperature": 0.3,
"repetition_penalty": 1.05, "max_new_token": 2048, "min_new_token": 0,
"show_tqdm": true, "ensure_non_empty": true, "stream_batch": true,
"spk_emb": null
}
}
需要指出一个细节:client.py 在请求体顶层还写了 audio_seed 与 text_seed 两个扁平字段,而 main.py 的 ChatTTSParams 并未声明它们——从源码结构看,Pydantic 默认会忽略未声明字段,因此这两个值不会进入模型;真正影响音色与随机性的入口是 params_infer_code.spk_emb / params_infer_code.manual_seed。
此外,仓库还提供了一个参数更全的命令行客户端 postScript.py:通过 --text(多词)、--audio_seed、--text_seed、--stream、--lang、--infer_top_P 等参数逐一对应请求体字段,并支持 --tgt 指定输出目录。注意它默认读取的服务端口是 9900(CHATTTS_SERVICE_PORT 默认值),与服务端示例的 8000 不同,调用前需确认环境变量。
启动 OpenAI 兼容服务 openai_api.py
官方文档提供了第二条启动命令:
fastapi dev examples/api/openai_api.py --host 0.0.0.0 --port 8000
openai_api.py 把 ChatTTS 包装成符合 OpenAI TTS 接口规范的网关,端点为 POST /v1/audio/speech,并附带 GET /health 健康检查(返回 {"status": "healthy", "model_loaded": true})。
请求字段与白名单过滤
请求模型 OpenAITTSRequest 的字段设计:
| 字段 | 类型 / 约束 | 说明 |
|---|---|---|
model |
str,必填 |
任意字符串,服务端统一改写为 tts-1 |
input |
str,必填,最长 2048 |
待合成文本 |
voice |
str = "default" |
音色选择,支持 default / alloy / echo |
response_format |
str = "mp3" |
音频格式:mp3、wav、ogg |
speed |
float = 1.0,范围 0.5–2.0 |
语速(见下方说明) |
stream |
bool = False |
是否流式返回音频流 |
output_format |
str = "mp3" |
输出格式 |
入口函数先执行 validate_request:把未知字段按 ALLOWED_PARAMS 白名单过滤并记录警告,再把剩余字段交给 Pydantic 校验。音频格式不在 {"mp3", "wav", "ogg"} 内会直接返回 400。异常统一走自定义处理器,包装为 OpenAI 风格的 {"error": {"message": ..., "type": ...}}(openai_api.py)。
音色映射与并发控制
启动事件里定义了三音色的映射表 VOICE_MAP:
VOICE_MAP = {
"default": "1528.pt",
"alloy": "1384.pt",
"echo": "2443.pt",
}
启动时会对当前工作目录下存在的 .pt 说话人嵌入文件逐个 torch.load 到内存预热(openai_api.py),不存在的音色仅打 warning 并跳过,请求时回退到 default。换言之,想要启用 alloy / echo,需要先把对应嵌入文件放到启动目录——文件本身不在仓库内(源码注释指向社区分享的下载渠道)。
并发方面,openai_api.py 用 asyncio.Lock(app.state.model_lock)串行化模型推理,避免多线程同时进入 PyTorch 算子导致的不确定性;推理参数在 openai_api.py 写死为:prompt="[speed_5]"、top_P=0.5、top_K=10、temperature=0.1、repetition_penalty=1.1、manual_seed=42、stream_batch=24。可以推断该配置偏向稳定、低方差的输出。注意 speed 字段到 [speed_N] prompt 的转换代码目前被注释(openai_api.py),即请求体里的 speed 暂不实际改变语速。
流式 WAV 输出
stream=true 时,服务端用 StreamingResponse 逐块下发;若格式为 wav,第一个块前会插入一个手工构造的 WAV 头(generate_wav_header:RIFF/WAVE/fmt 段,采样率 24000、16bit、单声道),数据段长度用 0xFFFFFFFF 占位,便于播放器边收边播。非流式 wav 则直接由 pcm_arr_to_wav_view 生成完整文件。
参数背后的推理管线:从 HTTP 字段到 Chat.infer
上面的两套服务最终都收敛到 Chat.infer。理解这条管线,能帮你判断每个 HTTP 参数值不值得改。
两组采样参数数据类
RefineTextParams 与 InferCodeParams 是定义在 core.py 的 dataclass,后者继承前者并覆盖关键默认值:
| 参数 | RefineTextParams 默认 | InferCodeParams 默认 | 作用 |
|---|---|---|---|
prompt |
"" |
"[speed_5]" |
控制提示;[speed_N] 形式可调节语速档位 |
top_P / top_K |
0.7 / 20 | 0.1 / 20 | 采样截断;推理侧更窄 |
temperature |
0.7 | 0.3 | 推理侧更确定 |
repetition_penalty |
1.0 | 1.05 | 抑制重复 |
max_new_token |
384 | 2048 | 精修文本 vs 声学码本长度上限不同 |
stream_batch |
—(继承 0) | 24 | 流式时按批下发的批大小 |
manual_seed |
None |
— | 固定随机性 |
spk_emb / spk_smp / txt_smp |
— | None |
说话人嵌入、参考音频 prompt、参考文本 |
stream_speed / pass_first_n_batches |
— | 12000 / 2 | 流式步进与跳过的首批数 |
文本切分、归一化与多句音色一致性
infer 的实际流程(core.py 与 core.py):
- 切分:
split_text=True时,若输入是单个字符串,优先按\n或句子边界(。与英文句号)切句; - 归一化:每条文本经过
normalizer,对应请求里的do_text_normalization/do_homophone_replacement/lang; - 文本精修:
skip_refine_text=False时用 GPT 的文本分支改写输入,得到更口语化的措辞(ensure_non_empty保证输出非空); - 参考句机制:多句输入且未提供
spk_smp时,会先合成第一句,再用 sample_audio_speaker(DVAE 编码音频得到说话人嵌入)回填spk_smp与txt_smp,保证后续句子音色、语域与前句一致——这就是多句文本听起来像同一个人连续说话的底层原因; - 解码:
_infer_code生成声学码本(GPT 分支),再经 decoder(或 dvae)出 mel 谱,最后由 Vocos 解出波形;use_decoder决定走哪条解码路径。
关于流式:stream=True 时 _infer 按 pass_first_n_batches 跳过首批后,以 stream_speed(采样点数)为步进 yield 音频块,main.py 会把每个块分别编码成独立 mp3 装入 zip——因此原生接口的流式结果是一组小 mp3,而非单一音频流;如果你需要“边生成边播放”的体验,OpenAI 兼容接口的 stream=true + response_format=wav 组合更贴合播放器场景。
说话人控制的两条路径
- 随机音色:
main.py中manual_seed触发chat.sample_random_speaker()(core.py),从预训练的 spk_stat 分布采样嵌入; - 指定音色:在原生接口里通过
params_infer_code.spk_emb传嵌入字符串;在 OpenAI 接口里通过voice字段映射到本地.pt文件。
嵌入如何生效可以在 _infer_code 看到:speaker.apply 会把 spk_emb 对应的向量写入 token 序列中说话人占位符的嵌入位置,从而整体平移音色。
部署注意事项小结
- 端口与环境变量:客户端 client.py 默认打
localhost:8000,postScript.py 默认打127.0.0.1:9900;跨机器部署时用CHATTTS_SERVICE_HOST/CHATTTS_SERVICE_PORT统一覆盖。 - 模型下载:
source="huggingface"首次启动即下载,受HF_HOME控制缓存位置(默认~/.cache/huggingface,见 core.py);内网环境可改用local/custom来源并准备custom_path。 - macOS 用户:两个服务端都会自动设置 MPS fallback;Vocos 在 MPS/NPU 上固定回退 CPU 执行(core.py),属预期行为。
- 校验与排错:原生接口对非法字段返回 422 +
detail明细;OpenAI 接口的错误统一为error.message/type结构,便于按 OpenAI SDK 的习惯处理。 - 合规:模型权重为 CC BY-NC 4.0,代码为 AGPLv3+(见 README.md),服务化部署前请确认用途符合许可约定。
沿着本文路径——安装依赖、fastapi dev 启动服务端、python examples/api/client.py 触发合成并查看 output 目录下的 mp3——即可完整复现官方 examples/api/README.md 描述的全流程;若要把 ChatTTS 接入既有 OpenAI 生态,切换到 openai_api.py 网关即可,两者的底层推理管线与参数默认值均由 ChatTTS/core.py 统一决定。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00