服务定位
sherpa-tts-server 是一个 C++ HTTP TTS 服务,默认基于 sherpa-onnx 离线 Kokoro 中英双语多音色模型(kokoro-multi-lang-v1_1,103 个说话人),也可通过配置切回 Matcha(matcha-icefall-zh-en)或 VITS(vits-zh-aishell3)。服务直接调用仓库内 sherpa-onnx 预编译库,合成 wav 后落盘并通过 HTTP 暴露下载 URL。
设计目标:
- 固定大小 HTTP worker 线程池
- 多实例
OfflineTts池并行合成 - 同
text + speakerId + speed的请求去重:in-flight 共享 + 文件级缓存 GET /wav/...流式发送,避免大文件占内存POST /tts-stream+GET /tts-stream/<id>.mp3支持边合成边播放HEAD /wav/...支持,便于 FreeSWITCHplayback拉取前预检- 启动时可选扫描
public/wav/重建文件索引
面向 callflow-esl 的 ctx.speak({ kind: "tts" }) 场景,但接口本身是通用 HTTP。
构建与运行
1 | cd onnx-platform\sherpa-tts-server |
构建目录保留在 build\win_x64\,**可运行的发布产物安装到 target\win_x64\**(仅含可执行文件、config.json 与运行时 DLL,不含编译中间产物)。bun run dev / bun run dev:tts 即从这里启动它。
启动:
1 | cd .\target\win_x64 |
模型目录解析顺序:
- 当前工作目录的上三级
../../../models(典型target\win_x64\启动场景) - 当前工作目录的上一级
../models(源码目录启动场景) - 否则回退到当前工作目录下的
./models
默认只要找到 models/kokoro-multi-lang-v1_1/ 即可启动。
默认 Kokoro 依赖文件:
model.onnxvoices.bintokens.txtlexicon-us-en.txt、lexicon-zh.txtdict/espeak-ng-data/
切回 Matcha 时需要 model-steps-3.onnx、vocos-16khz-univ.onnx、tokens.txt、lexicon.txt、espeak-ng-data/;切回 VITS 时需要 vits-aishell3.onnx、tokens.txt、lexicon.txt。
HTTP 端点
| Method | Path | 用途 |
|---|---|---|
| GET | /health |
健康检查 + 当前已加载模型能力(model.numSpeakers / supportsMultipleSpeakers / speakers[] 等) |
| POST | /tts |
提交合成请求,等待整段 wav 合成完成后返回 wavUrl |
| POST | /tts-stream |
创建流式 TTS 播放链接(不等待完整合成) |
| GET | /tts-stream/<id>.mp3 |
在同一个连接中以 Transfer-Encoding: chunked 持续推 MP3 |
| HEAD | /tts-stream/<id>.mp3 |
检查流式链接是否存在 |
| GET | /wav/<file> |
下载已合成的 wav |
| HEAD | /wav/<file> |
预检 wav 是否存在 |
所有响应带 Connection: close,错误统一 JSON:
1 | { "error": "<message>" } |
GET /health
返回服务基础信息与当前已加载 TTS 模型能力。前端(如管理控制台的音色试听)用model.numSpeakers > 1 或 model.supportsMultipleSpeakers=true 判断是否展示多个 speakerId 试听项。
1 | { |
默认 Kokoro 是 103 个说话人的多音色模型,
speakerId直接对应/health返回的model.speakers[].id。
POST /tts
请求:
1 | curl -s -X POST http://127.0.0.1:9080/tts \ |
| 字段 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
text |
string | 是 | - | 要合成的文本,trim 后不能为空 |
speakerId |
int | 否 | 0 | 多说话人 ID;超过 NumSpeakers() 自动回退 0 |
speed |
float | 否 | 1.0 | 合成语速倍率,必须 > 0 |
成功响应(HTTP 200):
1 | { |
| 字段 | 说明 |
|---|---|
wavUrl |
publicBaseUrl + /wav/ + fileName |
fileName |
格式 tts-<FNV-1a 16hex>.wav |
sampleRate |
模型采样率 |
cached |
true 表示命中已有 wav,未触发新合成 |
错误响应:
| HTTP | 触发 | 示例 body |
|---|---|---|
| 400 | 非 JSON、缺 text、text 为空、speed≤0、Content-Length 超限 | {"error":"Missing text"} |
| 404 | 路径不是 /tts 或 /wav/* |
{"error":"Not found"} |
| 500 | TTS 生成失败、wav 写盘失败 | {"error":"TTS generated empty audio"} |
| 503 | 等待队列已满(受 maxQueuedRequests) |
{"error":"Server busy"} |
缓存与去重
服务按以下 key 做 FNV-1a 64bit 哈希得到 tts-<16hex>.wav:
1 | <ttsModelType>:<ttsModelDir> | sid=<speakerId> | speed=<speed.fixed3> | text=<text> |
同样的 {text, speakerId, speed} 永远命中同一文件:
- 进程内已知 → 直接返回
cached=true - in-flight 合成中 → 共享同一个 future,等待其他请求完成后一起返回
- 否则 → 当前请求成为 owner,调用
OfflineTtsWorker实际生成
注意
当前没有 LRU / TTL 清理逻辑,wav 会持续保留在 public\wav\。长期运行需要外部任务定期清理;删除文件不会破坏服务,下一次相同请求会重新生成。
GET /wav/<fileName>
1 | HTTP/1.1 200 OK |
约束:
fileName只取path::filename,禁止..,否则 400- 不在缓存索引中返回 404
- 仅返回
Content-Length头,不支持 Range / 多段断点续传
HEAD /wav/<fileName>
与 GET 完全一致的状态码和头部,但不返回 body。FreeSWITCH playback 拉取 wav URL 时通常会先做 HEAD 检查,文件不存在直接放弃,避免下载半截。
POST /tts-stream(流式 TTS 入口)
请求体与 POST /tts 完全相同:
1 | curl -s -X POST http://127.0.0.1:9080/tts-stream \ |
立即返回(无需等待整段合成):
1 | { |
GET /tts-stream/<id>.mp3 在同一个 HTTP 连接里通过 Transfer-Encoding: chunked 输出 audio/mpeg:服务端调用 sherpa TTS 的增量回调,把新生成的 float PCM 样本送入 ffmpeg 实时编码为 MP3 并写入 HTTP 响应;完整生成后仍会把 wav 结果落盘到 public/wav/,供普通 /tts 链路缓存命中。
HEAD /tts-stream/<id>.mp3 只校验链接是否存在,不触发合成。
注意
流式链路依赖外部 ffmpeg 进程做 PCM → MP3 实时编码,部署时务必保证 mp3EncoderPath(默认 "ffmpeg")对应的二进制可执行。FreeSWITCH 侧通常用 shout://... 前缀通过 mod_shout 拉取,参考 apps/callflow-esl/config.json 的 tts.streamPlaybackPrefix。
关键配置
| 字段 | 默认 | 说明 |
|---|---|---|
listenHost / listenPort |
127.0.0.1 / 9080 |
实际监听地址 |
publicBaseUrl |
http://127.0.0.1:9080 |
返回给客户端的 wavUrl 前缀;可与 listenHost 不同(反代后) |
workerThreads |
CPU 核心数 | HTTP 请求处理线程数 |
ttsPoolSize |
max(CPU/2, 1) | 预热的 OfflineTts 实例数 |
mp3EncoderPath |
ffmpeg |
流式 TTS 把 PCM 编码为 MP3 时使用的 ffmpeg 路径;mod_shout 拉流必需 |
mp3BitrateKbps |
192 | 流式 TTS 转 MP3 的 CBR 码率,范围 64..320 |
mp3VolumeGainDb |
3.0 | 流式 TTS 转 MP3 时追加的响度增益(dB),范围 -20..20;正增益会同时启用 limiter 降低削波 |
wavSendChunkBytes |
65536 | GET /wav/... 流式发送块大小 |
maxRequestBodyBytes |
1048576 | 单请求体最大字节数 |
maxQueuedRequests |
0(不限) | 等待队列上限,超出直接 503 |
startupScanCache |
true | 启动时扫描 public/wav/ 建文件索引 |
ttsModelType |
kokoro |
TTS 模型类型:kokoro、matcha 或 vits |
ttsModelDir |
kokoro-multi-lang-v1_1 |
相对 onnx-platform/models/ 的模型目录,也可填绝对路径 |
ttsKokoro* |
见 README | Kokoro 模型文件名(model.onnx / voices.bin / tokens.txt / lexicon-us-en.txt,lexicon-zh.txt / espeak-ng-data / dict 等) |
ttsMatcha* / ttsVits* |
见 README | 切回 Matcha / VITS 时的对应模型文件名 |
缓存 wav 与实时合成两条流式路径共用同一套
mp3BitrateKbps/mp3VolumeGainDb,避免首次播放与缓存播放的 MP3 听感不一致。完整字段表见仓库onnx-platform/sherpa-tts-server/README.md。
并发模型
1 | accept thread (单线程) |
并发吞吐主要取决于:
ttsPoolSize:实际并行合成度- 模型本身的 RTF(输出
rtf=日志可对比) workerThreads:HTTP 处理线程数,通常>= ttsPoolSize即可
缓存命中 / in-flight 复用时不占 TTS 实例,只做 I/O。
与 FreeSWITCH 集成
callflow-esl 配置:
1 | { |
业务调用 ctx.speak({ kind: "tts", text: "..." }) 时,runtime:
streamingEnabled=false时,向本服务POST /tts拿到wavUrl,再让 FreeSWITCHplayback <wavUrl>streamingEnabled=true时,向本服务POST /tts-stream拿到streamUrl,把streamPlaybackPrefix拼到链接前(默认shout://)交给 FreeSWITCH 边拉边播- 完整 wav 仍会落盘到
public/wav/,下一次相同{text, speakerId, speed}会直接命中缓存
playbackTarget 两种模式
| 取值 | 行为 |
|---|---|
"wav-url"(默认) |
把 wavUrl 直接交给 FS playback,FS 自己拉 |
"file-path" |
用 tts.fsPlaybackBaseDir + fileName 拼成 FS 可访问的本地路径(要求 callflow-esl 与 FS 同机 / 共享卷) |
publicBaseUrl 在远端 FS 场景下的注意点
注意
如果 callflow-esl 部署在机器 A、FS 部署在机器 B、TTS 服务部署在机器 A:
listenHost=0.0.0.0(接受外部连接)publicBaseUrl=http://A 的可访问 IP:9080(不能填 127.0.0.1,否则 FS 在 B 上拉不到)
FS 必须能从自己网络通到 publicBaseUrl。
调用示例
curl
1 | curl -s -X POST http://127.0.0.1:9080/tts \ |
Node / Bun
仓库内 request-tts.js:
1 | bun run .\request-tts.js |
等价:
1 | const res = await fetch("http://127.0.0.1:9080/tts", { |
输出位置
1 | target\win_x64\public\wav\tts-<hash>.wav |
publicBaseUrl 对外暴露的 /wav/... 实际就读这个目录。
日志
所有日志以 [sherpa_tts_server] 前缀输出到 stdout。关键事件:
| 日志 | 含义 |
|---|---|
loaded TTS worker in <ms> ms |
单个 TTS 实例加载完成 |
listening on http://<host>:<port> |
监听就绪 |
generated <path> in <ms> ms (rtf=<n>) |
本次合成耗时与实时倍率 |
request textLength=<n> cached=<bool> elapsedMs=<n> |
一次 /tts 请求摘要 |
startup cache scanned <n> wav files |
启动扫描结果 |
局限
注意
- 仅支持最小 JSON 解析,不支持复杂嵌套结构和数组
- 缺少任何模型文件会在启动时抛
missing TTS model file: ...或missing TTS model directory: ...并退出 - 默认
kokoro-multi-lang-v1_1是 103 说话人的多音色模型,speakerId直接对应/health返回的model.speakers[].id;切回matcha-icefall-zh-en则为单说话人 - 启动扫描无法可靠恢复原始 cache_key,扫描出的文件只能通过 GET/HEAD 按文件名访问;新的
/tts请求若文本相同仍会命中合成路径(hash 一致),生成结果与扫描到的旧文件同名并覆盖 - 服务无 graceful shutdown:Ctrl+C 直接终止 accept 与 worker
- 无鉴权、无限流(除
maxQueuedRequests),仅建议内网 / 可信网络运行 - 无 LRU / TTL;长期运行需要外部清理
public/wav/
参考
- 详细字段说明:仓库
onnx-platform/sherpa-tts-server/README.md - callflow-esl 集成:callflow-esl