跳到正文

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.tsTtsConfigTtsSpeakInput 等公开类型

callflow-esl 配置

配置位于 apps/callflow-esl/config.jsontts 节,也可被数据库运行时配置 覆盖。字段如下:

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.requestTimeoutMscallflow-esl 请求 TTS 服务的 HTTP 超时,单位毫秒
use.call单呼 ctx.speak({ kind: "tts" }) 使用的 profile:wavshout
use.conference会议播放 / 注入使用的 profile:wavshout
profiles.wav.endpoint普通 WAV 合成接口地址,对应 sherpa-tts-serverPOST /tts
profiles.wav.playbackTargetWAV 模式下的播放目标生成策略:wav-urlfile-path
profiles.wav.fsPlaybackBaseDirplaybackTarget=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.speakerId
  • speed 优先使用业务入参,未传则使用 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 时会直接交给 FreeSWITCH playback
  • fileName:不能为空。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-pathfsPlaybackBaseDir 不能为空,否则 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:不能为空,会被转换成 FreeSWITCH playback 目标

诊断字段:

  • 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,通常配合 FreeSWITCH mod_shout 播放
  • 流式链路未命中缓存时仍会在生成完成后落盘 WAV,后续 /tts/tts-stream 可复用缓存
  • 流式 WAV→MP3 会按 mp3VolumeGainDb(默认 +3.0 dB,范围 -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 empty
  • repeat 必须是正整数
  • speakerIdspeed 仅影响本次请求,不会修改全局配置

日志与错误

普通 /tts 成功时,callflow-esl 写入:

text
tts_http_resolved

常见字段包括:

  • endpoint
  • textLength
  • speakerId
  • speed
  • wavUrl
  • fileName
  • sampleRate
  • cached
  • elapsedMs

普通 /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 ...msTTS 服务未响应或合成耗时超过 requestTimeoutMs确认服务健康、调大 requestTimeoutMs、缩短单次文本
HTTP 400 Bad Request请求体不合法,例如文本为空或 speed <= 0检查业务传入的 textspeed
HTTP 503 Service UnavailableTTS 服务端队列已满降低并发或调大 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_shoutffmpeg 不可用启用 mod_shout,确认 TTS 服务端 mp3EncoderPath

联调检查清单

  1. 启动 sherpa-tts-server,确认 GET /health 返回 status=ok
  2. 直接请求 /tts,确认返回 wavUrlfileName,并且浏览器或 curl 能访问 wavUrl
  3. 若启用流式模式,直接请求 /tts-stream,确认返回 streamUrl
  4. 在 FreeSWITCH 所在机器上访问 wavUrlstreamUrl,确认网络可达
  5. 流式模式确认 FreeSWITCH 已加载 mod_shout
  6. 启动 callflow-esl 后通过测试业务执行 ctx.speak({ kind: "tts" })
  7. 查看 callflow-esl 日志中的 tts_http_resolvedtts_stream_link_resolved
  8. 若任务中启动了任何服务,联调结束后停止对应服务进程

文档与代码在同一仓库维护,现有 Markdown 是唯一内容源。