服务定位

sherpa-tts-server 是一个 C++ HTTP TTS 服务,默认基于 sherpa-onnx 离线 Kokoro 中英双语多音色模型kokoro-multi-lang-v1_1103 个说话人),也可通过配置切回 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/... 支持,便于 FreeSWITCH playback 拉取前预检
  • 启动时可选扫描 public/wav/ 重建文件索引

面向 callflow-eslctx.speak({ kind: "tts" }) 场景,但接口本身是通用 HTTP。

构建与运行

1
2
3
4
5
6
cd onnx-platform\sherpa-tts-server
.\build.ps1
# 等价于:
# cmake -S . -B build/win_x64 -G "Visual Studio 17 2022" -A x64
# cmake --build build/win_x64 --config Release --target sherpa_tts_server
# cmake --install build/win_x64 --config Release --prefix target/win_x64

构建目录保留在 build\win_x64\,**可运行的发布产物安装到 target\win_x64\**(仅含可执行文件、
config.json 与运行时 DLL,不含编译中间产物)。bun run dev / bun run dev:tts 即从这里启动它。

启动:

1
2
3
4
cd .\target\win_x64
.\sherpa_tts_server.exe
# 或显式指定配置:
# .\sherpa_tts_server.exe C:\path\to\config.json

模型目录解析顺序:

  1. 当前工作目录的上三级 ../../../models(典型 target\win_x64\ 启动场景)
  2. 当前工作目录的上一级 ../models(源码目录启动场景)
  3. 否则回退到当前工作目录下的 ./models

默认只要找到 models/kokoro-multi-lang-v1_1/ 即可启动。

默认 Kokoro 依赖文件:

  • model.onnx
  • voices.bin
  • tokens.txt
  • lexicon-us-en.txtlexicon-zh.txt
  • dict/
  • espeak-ng-data/

切回 Matcha 时需要 model-steps-3.onnxvocos-16khz-univ.onnxtokens.txtlexicon.txt
espeak-ng-data/;切回 VITS 时需要 vits-aishell3.onnxtokens.txtlexicon.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 > 1model.supportsMultipleSpeakers=true 判断是否展示多个 speakerId 试听项。

1
2
3
4
5
6
7
8
9
10
11
12
13
{
"status": "ok",
"server": "sherpa-tts-server",
"listenPort": 9080,
"sampleRate": 24000,
"model": {
"type": "kokoro",
"dir": "kokoro-multi-lang-v1_1",
"numSpeakers": 103,
"supportsMultipleSpeakers": true,
"speakers": [ { "id": 0, "name": "af_maple" }, { "id": 1, "name": "af_sol" } ]
}
}

默认 Kokoro 是 103 个说话人的多音色模型,speakerId 直接对应 /health 返回的 model.speakers[].id

POST /tts

请求:

1
2
3
curl -s -X POST http://127.0.0.1:9080/tts \
-H "Content-Type: application/json" \
-d '{"text":"欢迎进入会议","speakerId":0,"speed":1.0}'
字段 类型 必填 默认 说明
text string - 要合成的文本,trim 后不能为空
speakerId int 0 多说话人 ID;超过 NumSpeakers() 自动回退 0
speed float 1.0 合成语速倍率,必须 > 0

成功响应(HTTP 200):

1
2
3
4
5
6
{
"wavUrl": "http://127.0.0.1:9080/wav/tts-3a1c7b9eaf0c1d72.wav",
"fileName": "tts-3a1c7b9eaf0c1d72.wav",
"sampleRate": 16000,
"cached": false
}
字段 说明
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} 永远命中同一文件

  1. 进程内已知 → 直接返回 cached=true
  2. in-flight 合成中 → 共享同一个 future,等待其他请求完成后一起返回
  3. 否则 → 当前请求成为 owner,调用 OfflineTtsWorker 实际生成

注意

当前没有 LRU / TTL 清理逻辑,wav 会持续保留在 public\wav\。长期运行需要外部任务定期清理;删除文件不会破坏服务,下一次相同请求会重新生成。

GET /wav/<fileName>

1
2
3
4
5
6
HTTP/1.1 200 OK
Content-Type: audio/wav
Content-Length: <bytes>
Connection: close

<wav binary stream,按 wavSendChunkBytes 分块发送>

约束:

  • 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
2
3
curl -s -X POST http://127.0.0.1:9080/tts-stream \
-H "Content-Type: application/json" \
-d '{"text":"欢迎进入会议","speakerId":0,"speed":1.0}'

立即返回(无需等待整段合成):

1
2
3
4
5
6
{
"streamUrl": "http://127.0.0.1:9080/tts-stream/3a1c7b9eaf0c1d72.mp3",
"streamPath": "/tts-stream/3a1c7b9eaf0c1d72.mp3",
"sampleRate": 16000,
"cached": false
}

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.jsontts.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 模型类型:kokoromatchavits
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
2
3
4
5
6
7
8
9
10
11
12
13
accept thread (单线程)


queue_ ────► worker[0..workerThreads-1]
│ 解析 HTTP、路由

TtsService::Synthesize

▼ Acquire / Release
OfflineTtsPool (ttsPoolSize 实例)


Generate + WriteWave

并发吞吐主要取决于:

  • ttsPoolSize:实际并行合成度
  • 模型本身的 RTF(输出 rtf= 日志可对比)
  • workerThreads:HTTP 处理线程数,通常 >= ttsPoolSize 即可

缓存命中 / in-flight 复用时不占 TTS 实例,只做 I/O。

与 FreeSWITCH 集成

callflow-esl 配置:

1
2
3
4
5
6
7
8
9
10
11
{
"tts": {
"sherpaHttpEndpoint": "http://127.0.0.1:9080/tts",
"streamingEnabled": true,
"sherpaStreamEndpoint": "http://127.0.0.1:9080/tts-stream",
"streamPlaybackPrefix": "shout://",
"playbackTarget": "wav-url",
"speakerId": 0,
"speed": 1
}
}

业务调用 ctx.speak({ kind: "tts", text: "..." }) 时,runtime:

  1. streamingEnabled=false 时,向本服务 POST /tts 拿到 wavUrl,再让 FreeSWITCH playback <wavUrl>
  2. streamingEnabled=true 时,向本服务 POST /tts-stream 拿到 streamUrl,把 streamPlaybackPrefix 拼到链接前(默认 shout://)交给 FreeSWITCH 边拉边播
  3. 完整 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
2
3
curl -s -X POST http://127.0.0.1:9080/tts \
-H "Content-Type: application/json" \
-d '{"text":"测试语音合成","speakerId":0,"speed":1.0}'

Node / Bun

仓库内 request-tts.js

1
2
3
bun run .\request-tts.js
bun run .\request-tts.js "测试语音合成"
bun run .\request-tts.js "测试语音合成" http://127.0.0.1:9080/tts

等价:

1
2
3
4
5
6
7
const res = await fetch("http://127.0.0.1:9080/tts", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: "测试语音合成" }),
});
const result = await res.json();
console.log(result.wavUrl);

输出位置

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