首页
/ ChatTTS API 服务部署实战:FastAPI 原生接口与 OpenAI 兼容 TTS 网关

ChatTTS API 服务部署实战:FastAPI 原生接口与 OpenAI 兼容 TTS 网关

2026-09-05 09:27:20作者:宣海椒Queenly

本文基于仓库中 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/speechGET /health,请求字段对齐 OpenAI TTS 规范
client.py 原生客户端 requests 直接请求 /generate_voice,解压结果到 output 目录
postScript.py 命令行客户端 通过 argparse 暴露全部参数,适合脚本化调用
requirements.txt 依赖清单 fastapirequests 两项

两个服务端共享同一套底座:启动时实例化 ChatTTS.Chat,注册中英文文本归一化器,再从 HuggingFace 拉取模型。区别在于参数暴露方式——main.py 把底层数据类参数原样交给调用方,而 openai_api.py 把参数收敛为 OpenAI 风格的白名单字段,内部写死一套经过调优的采样配置。

环境准备

官方文档给出的安装步骤是:

pip install -r examples/api/requirements.txt

其中 examples/api/requirements.txt 只包含 fastapirequests。但要让服务端真正跑起来,还需要仓库根依赖 requirements.txt,其核心包括:

  • torch>=2.1.0torchaudionumpy<3.0.0:推理运行时;
  • transformers>=4.41.1vocosvector_quantize_pytorch:Vocos 声码器与 DVAE 重建所需;
  • pynini==2.1.5WeTextProcessingnemo_text_processing:仅 Linux 平台(sys_platform == 'linux')安装,分别支撑中文与英文文本归一化。

这里有一个容易踩坑的点:main.pyopenai_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.pytools/normalizer/zh.py。若在 Linux 上未安装 nemo_text_processing / WeTextProcessing,而请求又开启 do_text_normalization=true(默认开启),文本预处理会不可用。因此建议先安装根依赖再启动服务。

模型本身不需要手动下载:两个服务端均调用 chat.load(source="huggingface"),从源码 ChatTTS/core.pydownload_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 完成了三件事:

  1. 实例化 ChatTTS.Chat 并注册中英文归一化器;
  2. chat.load(source="huggingface") 下载并加载 vocos、dvae、embed、gpt、speaker、decoder、tokenizer 等模块,加载失败直接 sys.exit(1)
  3. 另外还设置了平台兼容开关——macOS 下导出 PYTORCH_ENABLE_MPS_FALLBACK=1main.py),因为源码中 Vocos 在 MPS 设备上会回退到 CPU 执行(core.py 有对应注释)。

接口与请求模型

服务只暴露一个端点 POST /generate_voice,请求体由 Pydantic 模型 ChatTTSParams 定义,字段与 Chat.infer 的签名一一对应:

字段 类型 / 默认值 说明
text list[str],必填 待合成文本列表,每项通常是一句
stream bool = False 是否流式;为 Truechat.infer 返回生成器,服务端会把每个流式块分别编码为 0.mp31.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 的行为:

  1. 从环境变量 CHATTTS_SERVICE_HOST / CHATTTS_SERVICE_PORT 读取服务地址(默认 localhost:8000),拼出 /generate_voice URL;
  2. POST 一个示例请求体(中文双句文本),其中关键默认值:stream=Falseskip_refine_text=Truedo_text_normalization=True
  3. 把返回的 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_seedtext_seed 两个扁平字段,而 main.pyChatTTSParams 并未声明它们——从源码结构看,Pydantic 默认会忽略未声明字段,因此这两个值不会进入模型;真正影响音色与随机性的入口是 params_infer_code.spk_emb / params_infer_code.manual_seed

此外,仓库还提供了一个参数更全的命令行客户端 postScript.py:通过 --text(多词)、--audio_seed--text_seed--stream--lang--infer_top_P 等参数逐一对应请求体字段,并支持 --tgt 指定输出目录。注意它默认读取的服务端口是 9900CHATTTS_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" 音频格式:mp3wavogg
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.pyasyncio.Lockapp.state.model_lock)串行化模型推理,避免多线程同时进入 PyTorch 算子导致的不确定性;推理参数在 openai_api.py 写死为:prompt="[speed_5]"top_P=0.5top_K=10temperature=0.1repetition_penalty=1.1manual_seed=42stream_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 参数值不值得改。

两组采样参数数据类

RefineTextParamsInferCodeParams 是定义在 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.pycore.py):

  1. 切分split_text=True 时,若输入是单个字符串,优先按 \n 或句子边界( 与英文句号)切句;
  2. 归一化:每条文本经过 normalizer,对应请求里的 do_text_normalization / do_homophone_replacement / lang
  3. 文本精修skip_refine_text=False 时用 GPT 的文本分支改写输入,得到更口语化的措辞(ensure_non_empty 保证输出非空);
  4. 参考句机制:多句输入且未提供 spk_smp 时,会先合成第一句,再用 sample_audio_speaker(DVAE 编码音频得到说话人嵌入)回填 spk_smptxt_smp,保证后续句子音色、语域与前句一致——这就是多句文本听起来像同一个人连续说话的底层原因;
  5. 解码_infer_code 生成声学码本(GPT 分支),再经 decoder(或 dvae)出 mel 谱,最后由 Vocos 解出波形;use_decoder 决定走哪条解码路径。

关于流式:stream=True_inferpass_first_n_batches 跳过首批后,以 stream_speed(采样点数)为步进 yield 音频块,main.py 会把每个块分别编码成独立 mp3 装入 zip——因此原生接口的流式结果是一组小 mp3,而非单一音频流;如果你需要“边生成边播放”的体验,OpenAI 兼容接口的 stream=true + response_format=wav 组合更贴合播放器场景。

说话人控制的两条路径

  • 随机音色main.pymanual_seed 触发 chat.sample_random_speaker()core.py),从预训练的 spk_stat 分布采样嵌入;
  • 指定音色:在原生接口里通过 params_infer_code.spk_emb 传嵌入字符串;在 OpenAI 接口里通过 voice 字段映射到本地 .pt 文件。

嵌入如何生效可以在 _infer_code 看到:speaker.apply 会把 spk_emb 对应的向量写入 token 序列中说话人占位符的嵌入位置,从而整体平移音色。

部署注意事项小结

  1. 端口与环境变量:客户端 client.py 默认打 localhost:8000postScript.py 默认打 127.0.0.1:9900;跨机器部署时用 CHATTTS_SERVICE_HOST / CHATTTS_SERVICE_PORT 统一覆盖。
  2. 模型下载source="huggingface" 首次启动即下载,受 HF_HOME 控制缓存位置(默认 ~/.cache/huggingface,见 core.py);内网环境可改用 local / custom 来源并准备 custom_path
  3. macOS 用户:两个服务端都会自动设置 MPS fallback;Vocos 在 MPS/NPU 上固定回退 CPU 执行(core.py),属预期行为。
  4. 校验与排错:原生接口对非法字段返回 422 + detail 明细;OpenAI 接口的错误统一为 error.message/type 结构,便于按 OpenAI SDK 的习惯处理。
  5. 合规:模型权重为 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 统一决定。

登录后查看全文
热门项目推荐
相关项目推荐