服务定位
sherpa-tts-server 是一个 C++ HTTP TTS 服务,基于 sherpa-onnx 离线 VITS 模型(vits-zh-aishell3)。直接调用仓库内 sherpa-onnx 预编译库,合成 wav 后落盘并通过 HTTP 暴露下载 URL。
设计目标:
- 固定大小 HTTP worker 线程池
- 多实例
OfflineTts池并行合成 - 同
text + speakerId + speed的请求去重:in-flight 共享 + 文件级缓存 GET /wav/...流式发送,避免大文件占内存HEAD /wav/...支持,便于 FreeSWITCHplayback拉取前预检- 启动时可选扫描
public/wav/重建文件索引
面向 callflow-esl 的 ctx.speak({ kind: "tts" }) 场景,但接口本身是通用 HTTP。
构建与运行
1 | cd sherpa-tts-server |
启动:
1 | cd .\build\Release |
模型目录解析顺序:
- 当前工作目录的上三级
../../../models(典型build/Release启动场景) - 否则回退到当前工作目录下的
./models
只要找到 models/vits-zh-aishell3/ 即可启动。
依赖文件:
vits-aishell3.onnxtokens.txtlexicon.txt
三个 HTTP 端点
| Method | Path | 用途 |
|---|---|---|
| POST | /tts |
提交合成请求 |
| GET | /wav/<file> |
下载已合成的 wav |
| HEAD | /wav/<file> |
预检 wav 是否存在 |
所有响应带 Connection: close,错误统一 JSON:
1 | { "error": "<message>" } |
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 |
模型采样率(VITS aishell3 通常 16000 或 22050) |
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 | vits-zh-aishell3 | 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 检查,文件不存在直接放弃,避免下载半截。
关键配置
| 字段 | 默认 | 说明 |
|---|---|---|
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 实例数 |
wavSendChunkBytes |
65536 | GET /wav/... 流式发送块大小 |
maxRequestBodyBytes |
1048576 | 单请求体最大字节数 |
maxQueuedRequests |
0(不限) | 等待队列上限,超出直接 503 |
startupScanCache |
true | 启动时扫描 public/wav/ 建文件索引 |
并发模型
1 | accept thread (单线程) |
并发吞吐主要取决于:
ttsPoolSize:实际并行合成度- 模型本身的 RTF(输出
rtf=日志可对比) workerThreads:HTTP 处理线程数,通常>= ttsPoolSize即可
缓存命中 / in-flight 复用时不占 TTS 实例,只做 I/O。
与 FreeSWITCH 集成
callflow-esl 配置:
1 | { |
业务调用 ctx.speak({ kind: "tts", text: "..." }) 时,runtime:
POST /tts拿到wavUrl- 通过 ESL 下发
playback shout://<wavUrl>(或本地路径,见下面playbackTarget)
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 | build\Release\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: ...并退出 - 启动扫描无法可靠恢复原始 cache_key,扫描出的文件只能通过 GET/HEAD 按文件名访问;新的
/tts请求若文本相同仍会命中合成路径(hash 一致),生成结果与扫描到的旧文件同名并覆盖 - 服务无 graceful shutdown:Ctrl+C 直接终止 accept 与 worker
- 无鉴权、无限流(除
maxQueuedRequests),仅建议内网 / 可信网络运行 - 无 LRU / TTL;长期运行需要外部清理
public/wav/
参考
- 详细字段说明:仓库
sherpa-tts-server/README.md - callflow-esl 集成:callflow-esl