外观
集成与运行
调用示例
curl
bash
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,兼容 Node 18+ 和 Bun:
powershell
bun run .\request-tts.js
bun run .\request-tts.js "测试语音合成"
bun run .\request-tts.js "测试语音合成" http://127.0.0.1:9080/tts脚本会打印完整 JSON 响应。等价的 fetch 调用:
js
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);FreeSWITCH 集成(callflow-esl)
callflow-esl 通过 tts.defaults、tts.use 和 tts.profiles 选择本服务的普通 WAV 或 流式 MP3 端点:
json
{
"tts": {
"defaults": {
"speakerId": 0,
"speed": 1,
"requestTimeoutMs": 10000
},
"use": {
"call": "shout",
"conference": "wav"
},
"profiles": {
"wav": {
"endpoint": "http://127.0.0.1:9080/tts",
"playbackTarget": "wav-url",
"fsPlaybackBaseDir": ""
},
"shout": {
"endpoint": "http://127.0.0.1:9080/tts-stream",
"playbackPrefix": "shout://"
}
}
}
}业务调用 ctx.speak({ kind: "tts", text: "..." }) 时,runtime 会:
- runtime 按通话场景从
tts.use解析 profile wavprofile 向本服务POST /tts获取wavUrlshoutprofile 向本服务POST /tts-stream获取streamUrl- 通过 ESL 给 FreeSWITCH 下发
playback,流式模式默认使用shout://<host>/<path>交给mod_shout拉取
输出
text
target\win_x64\public\wav\所有生成的 wav 文件按 tts-<hash>.wav 命名落盘到这里。publicBaseUrl 对外暴露的 /wav/... 实际就读取这个目录。
并发模型
- accept 线程在主线程,单线程接受连接
- 入队
queue_,由workerThreads个工作线程消费 - 工作线程解析 HTTP、调用
TtsService::Synthesize - 合成时通过
OfflineTtsPool::Acquire()拿到一个OfflineTts实例 独占完成Generate + WriteWave,结束后归还 - 缓存命中或复用进行中的合成时不占用 TTS 实例,仅做 I/O
并发吞吐主要取决于:
ttsPoolSize:实际并行合成度ttsModelNumThreads:单条请求的推理线程数,通常更直接影响该请求的 RTF- 模型本身的 RTF(输出
rtf=日志可对比) workerThreads:HTTP 处理线程数,通常>= ttsPoolSize即可
降低单条请求 RTF 时,优先调高 ttsModelNumThreads 并相应降低 ttsPoolSize, 避免 ttsPoolSize * ttsModelNumThreads 明显超过可用 CPU 核心数。建议从 ttsModelNumThreads=2, ttsPoolSize=2 开始压测;如果机器核心数充足,再试 ttsModelNumThreads=4, ttsPoolSize=1。若业务文本可复用,命中 cached=true 后不会再进入模型推理,RTF 成本为 0。
日志
服务通过 stdout 输出运行日志,所有日志以 [sherpa_tts_server] 前缀开头。 关键事件:
ffmpeg MP3 encoder available encoderPath=<path>:启动探测成功,流式 MP3 编码器可用ffmpeg MP3 encoder unavailable encoderPath=<path> reason=<reason>; ...:启动探测失败;服务仍继续启动,但/tts-streamMP3 播放不可用,普通/ttsWAV 合成不受影响loaded TTS worker in <ms> ms:单个 TTS 实例加载完成,包含model_num_threads=<n>listening on http://<host>:<port>:服务监听就绪request start method=<method> path=<path> activeStreams=<n> queueSize=<n>:HTTP 请求开始处理request end method=<method> path=<path> status=<code> elapsedMs=<n> activeStreams=<n> queueSize=<n> reason=<reason>:HTTP 请求处理结束generated audio in <ms> ms (rtf=<n>) modelId=<id> modelBackend=<backend> modelDir=<dir> modelNumThreads=<n> modelFile=<file>:本次合成耗时、实时倍率和模型信息request textLength=<n> cached=<bool> elapsedMs=<n> modelId=<id> modelBackend=<backend> modelDir=<dir> modelNumThreads=<n> modelFile=<file>:一次/tts请求摘要streamed textLength=<n> cached=<bool> elapsedMs=<n> modelId=<id> modelBackend=<backend> modelDir=<dir> modelNumThreads=<n> modelFile=<file>:一次/tts-stream播放摘要stream busy textLength=<n> activeStreams=<n> maxActiveStreams=<n>:流式 GET 超过并发上限,已返回 503MP3 streaming encoder watchdog closed stream after streamMaxDurationMs=<n>:流式 GET 达到最长连接时间,服务端主动断开ffmpeg MP3 ... exit timed out:ffmpeg 退出超时,服务已 kill 子进程并标记本次流失败startup cache scanned <n> wav files:启动扫描结果
注意事项
- 配置文件使用完整 JSON parser;HTTP 请求/响应仍保留轻量 JSON 解析和拼接
- 缺少任何模型文件会在启动时抛
missing TTS model file: ...或missing TTS model directory: ...并退出 - 服务加载配置后会用
mp3.encoderPath完成一次最小 MP3 编码,并使用timeouts.ffmpegExitMs限制等待时间。程序无法启动、编码返回非零或超时只会 输出错误级别的功能受限告警,不阻止服务监听;此时/tts-stream的 MP3 播放链路不可用,普通/ttsWAV 合成不受影响 kokoro-multi-lang-v1_1是 103 个说话人的多音色模型,切到对应 profile 后speakerId直接对应/health返回的model.speakers[].id- 启动扫描无法可靠恢复原始
cache_key,但新的/tts或/tts-stream请求会按相同 hash 查找磁盘上的同名 wav,命中时会补回索引并复用缓存 - 服务没有平滑关闭:
Ctrl+C会直接终止 accept 与工作线程 - 当前没有鉴权、没有限流(除
maxQueuedRequests),建议仅在内网/可信网络运行