外观
TTS 服务对接文档
本文说明 apps/callflow-esl 如何对接本地 sherpa-tts-server,以及业务侧 ctx.speak({ kind: "tts" }) 到 FreeSWITCH 播放之间的实际调用链。
当前 callflow-esl 不直接调用 FreeSWITCH 的 speak application。所有 TTS 文本都会先请求 sherpa-tts-server,拿到可播放的 WAV URL、本地 WAV 路径或 MP3 流式 URL 后,再通过 ESL 下发 FreeSWITCH playback。
调用链概览
text
业务代码
ctx.speak({ kind: "tts", text, speakerId?, speed? })
|
v
callflow-esl runtime
resolvePlaybackPlan()
|
+-- tts profile = wav --> POST /tts
| 返回 wavUrl/fileName
|
+-- tts profile = shout --> POST /tts-stream
返回 streamUrl
|
v
FreeSWITCH playback
wav-url : playback http://.../wav/tts-xxx.wav
file-path : playback /path/to/tts-xxx.wav
stream : playback shout://host:port/tts-stream/xxx.mp3相关实现入口:
src/runtime/tts/playback-planner.ts:把SpeakInput转成 FreeSWITCH playback 计划src/runtime/tts/tts-client.ts:请求sherpa-tts-server,校验响应并生成 playback 目标src/types/types.ts:TtsConfig、TtsSpeakInput等公开类型
callflow-esl 配置
配置位于 apps/callflow-esl/config.json 的 tts 节,也可被数据库运行时配置 覆盖。字段如下:
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://"
}
}
}
}| 字段 | 说明 |
|---|---|
defaults.speakerId | 业务未指定 speakerId 时使用的默认说话人 ID |
defaults.speed | 业务未指定 speed 时使用的默认语速 |
defaults.requestTimeoutMs | callflow-esl 请求 TTS 服务的 HTTP 超时,单位毫秒 |
use.call | 单呼 ctx.speak({ kind: "tts" }) 使用的 profile:wav 或 shout |
use.conference | 会议播放 / 注入使用的 profile:wav 或 shout |
profiles.wav.endpoint | 普通 WAV 合成接口地址,对应 sherpa-tts-server 的 POST /tts |
profiles.wav.playbackTarget | WAV 模式下的播放目标生成策略:wav-url 或 file-path |
profiles.wav.fsPlaybackBaseDir | playbackTarget=file-path 时使用的 FreeSWITCH 可访问目录 |
profiles.shout.endpoint | 流式播放链接创建接口地址,对应 POST /tts-stream |
profiles.shout.playbackPrefix | 流式 URL 交给 FreeSWITCH 前追加或转换的前缀,常用值为 shout:// |
旧版扁平字段仍可被运行时配置加载器兼容转换,但新配置和新文档统一使用 defaults/use/profiles 结构。
普通 WAV 模式
当当前场景选择 wav profile 时, runtime 会调用:
text
POST {tts.profiles.wav.endpoint}
Content-Type: application/json请求体:
json
{
"text": "欢迎测试语音合成",
"speakerId": 0,
"speed": 1
}字段来源:
text来自业务侧ctx.speak({ kind: "tts", text })speakerId优先使用业务入参,未传则使用config.tts.defaults.speakerIdspeed优先使用业务入参,未传则使用config.tts.defaults.speed
callflow-esl 期望响应:
json
{
"wavUrl": "http://127.0.0.1:9080/wav/tts-3a1c7b9eaf0c1d72.wav",
"fileName": "tts-3a1c7b9eaf0c1d72.wav",
"sampleRate": 16000,
"cached": false
}必需字段:
wavUrl:不能为空。playbackTarget=wav-url时会直接交给 FreeSWITCHplaybackfileName:不能为空。playbackTarget=file-path时会和fsPlaybackBaseDir拼接
诊断字段:
sampleRate:只写入日志,不参与播放决策cached:只写入日志,用于判断是否命中 TTS 服务端缓存
普通 WAV 播放目标
playbackTarget=wav-url 时:
text
FreeSWITCH playback http://127.0.0.1:9080/wav/tts-xxx.wav适用场景:
- FreeSWITCH 能直接访问
wavUrl sherpa-tts-server.config.json中的publicBaseUrl已配置成 FreeSWITCH 可达地址- 不希望 callflow-esl 与 FreeSWITCH 共享文件目录
playbackTarget=file-path 时:
text
fsPlaybackBaseDir = "/usr/local/freeswitch/sounds/tts"
fileName = "tts-xxx.wav"
FreeSWITCH playback /usr/local/freeswitch/sounds/tts/tts-xxx.wav适用场景:
sherpa-tts-server生成的 WAV 文件目录与 FreeSWITCH 同机或共享卷- FreeSWITCH 通过本地文件系统播放比 HTTP 拉取更稳定
注意事项:
playbackTarget=file-path时fsPlaybackBaseDir不能为空,否则 runtime 会抛错- 拼接后的路径会统一使用
/,FreeSWITCH 在 Windows 上也可接受正斜杠路径 callflow-esl不校验文件是否真实存在,FreeSWITCH 必须能访问该路径
流式 TTS 模式
当当前场景选择 shout profile 时, runtime 不等待完整 WAV 合成,而是调用:
text
POST {tts.profiles.shout.endpoint}
Content-Type: application/json请求体与 /tts 相同:
json
{
"text": "欢迎测试语音合成",
"speakerId": 0,
"speed": 1
}callflow-esl 期望响应:
json
{
"streamUrl": "http://127.0.0.1:9080/tts-stream/3a1c7b9eaf0c1d72.mp3",
"streamPath": "/tts-stream/3a1c7b9eaf0c1d72.mp3",
"sampleRate": 16000,
"cached": false
}必需字段:
streamUrl:不能为空,会被转换成 FreeSWITCHplayback目标
诊断字段:
streamPath:只写入日志,便于关联 TTS 服务端请求sampleRate:只写入日志cached:服务端可能返回,但当前callflow-esl不依赖该字段
默认 profiles.shout.playbackPrefix="shout://" 时,runtime 会把 HTTP URL 转成 FreeSWITCH mod_shout 可播放的 URL:
text
http://127.0.0.1:9080/tts-stream/abc.mp3
|
v
shout://127.0.0.1:9080/tts-stream/abc.mp3如果 playbackPrefix 为空,则直接把 streamUrl 交给 FreeSWITCH。如果 配置成其他非空前缀,则按字符串前缀拼接。
流式模式依赖:
- FreeSWITCH 侧需要启用
mod_shout - FreeSWITCH 必须能访问
streamUrl指向的主机和端口 sherpa-tts-server需要能找到ffmpeg,或在其config.json中通过mp3EncoderPath指定绝对路径
sherpa-tts-server 侧要求
sherpa-tts-server 默认监听 9080,常用配置:
json
{
"listenHost": "0.0.0.0",
"listenPort": 9080,
"publicBaseUrl": "http://192.168.2.246:9080",
"mp3EncoderPath": "ffmpeg",
"mp3VolumeGainDb": 3.0
}关键点:
publicBaseUrl必须填写 FreeSWITCH 可访问的地址,而不一定是listenHost。例如服务监听0.0.0.0,但返回 URL 应是http://192.168.2.246:9080/tts会生成并落盘 WAV,然后返回/wav/<fileName>URL/tts-stream只创建播放链接,真正音频输出发生在 FreeSWITCH 后续GET /tts-stream/<id>.mp3时- 流式 GET 响应为
Content-Type: audio/mpeg,通常配合 FreeSWITCHmod_shout播放 - 流式链路未命中缓存时仍会在生成完成后落盘 WAV,后续
/tts和/tts-stream可复用缓存 - 流式 WAV→MP3 会按
mp3VolumeGainDb(默认+3.0dB,范围-20..20)追加 响度增益,若觉得 MP3 播报偏轻 / 偏响在 sherpa-tts-server 端调整,详见其README.md
业务侧用法
独立播报:
ts
await ctx.speak({
kind: "tts",
text: "欢迎致电,请说出您的需求。",
});覆盖说话人和语速:
ts
await ctx.speak({
kind: "tts",
text: "语速稍快的提示音。",
speakerId: 0,
speed: 1.2,
});允许被 barge-in 打断:
ts
await ctx.speak({
kind: "tts",
text: "您可以在播报过程中直接说话。",
interruptible: true,
});作为 hear() 的提示音:
ts
const result = await ctx.hear({
prompt: {
kind: "tts",
text: "请说出您的需求。",
},
interruptible: true,
timeoutMs: 30000,
});输入约束:
text去空白后不能为空,否则 runtime 会抛SpeakInput.text must not be emptyrepeat必须是正整数speakerId、speed仅影响本次请求,不会修改全局配置
日志与错误
普通 /tts 成功时,callflow-esl 写入:
text
tts_http_resolved常见字段包括:
endpointtextLengthspeakerIdspeedwavUrlfileNamesampleRatecachedelapsedMs
普通 /tts 失败时,写入:
text
tts_http_failed并向上抛出:
text
sherpa-http tts failed: <message>流式 /tts-stream 成功时,写入:
text
tts_stream_link_resolved流式 /tts-stream 失败时,写入:
text
tts_stream_link_failed并向上抛出:
text
sherpa-http tts stream failed: <message>常见失败原因:
| 现象 | 可能原因 | 处理方式 |
|---|---|---|
Timed out after ...ms | TTS 服务未响应或合成耗时超过 requestTimeoutMs | 确认服务健康、调大 requestTimeoutMs、缩短单次文本 |
HTTP 400 Bad Request | 请求体不合法,例如文本为空或 speed <= 0 | 检查业务传入的 text、speed |
HTTP 503 Service Unavailable | TTS 服务端队列已满 | 降低并发或调大 maxQueuedRequests / ttsPoolSize |
response missing wavUrl | /tts 响应缺少 wavUrl | 检查是否对接了正确版本的 sherpa-tts-server |
response missing fileName | /tts 响应缺少 fileName | 检查 TTS 服务端响应结构 |
stream response missing streamUrl | /tts-stream 响应缺少 streamUrl | 检查流式接口版本和响应结构 |
| FreeSWITCH 播放 HTTP WAV 失败 | publicBaseUrl 对 FreeSWITCH 不可达 | 改成 FreeSWITCH 可访问的 IP/域名 |
| FreeSWITCH 播放流式 MP3 失败 | 未启用 mod_shout 或 ffmpeg 不可用 | 启用 mod_shout,确认 TTS 服务端 mp3EncoderPath |
联调检查清单
- 启动
sherpa-tts-server,确认GET /health返回status=ok - 直接请求
/tts,确认返回wavUrl、fileName,并且浏览器或 curl 能访问wavUrl - 若启用流式模式,直接请求
/tts-stream,确认返回streamUrl - 在 FreeSWITCH 所在机器上访问
wavUrl或streamUrl,确认网络可达 - 流式模式确认 FreeSWITCH 已加载
mod_shout - 启动
callflow-esl后通过测试业务执行ctx.speak({ kind: "tts" }) - 查看
callflow-esl日志中的tts_http_resolved或tts_stream_link_resolved - 若任务中启动了任何服务,联调结束后停止对应服务进程