跳到正文

TTS 服务对接文档 ​

本文说明 apps/callflow-esl 如何对接本地 sherpa-tts-server,以及业务侧 ctx.speak({ kind: "tts" }) 到 FreeSWITCH 播放之间的实际调用链。

当前 callflow-esl 不直接调用 FreeSWITCH 的 speak application。普通 TTS 先请求 sherpa-tts-server,拿到可播放的 WAV URL、本地 WAV 路径或 PCM / 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 = stream --> 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 audio_fork://http://host:port/tts-stream/xxx.pcm?rate=16000&channels=1

相关实现入口:

  • 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 配置 ​

字段、当前示例值和路径说明以 配置参考 为准。tts.use.call 选择单呼使用的 wav 或 stream profile;tts.use.conference 在 audio_fork:// 驱动下可使用 stream,若为 shout:// 则必须使用 wav。stream 是流式 profile 名称,实际播放协议由 tts.profiles.stream.playbackPrefix 决定:当前推荐为 audio_fork://,也兼容 shout://。

普通 WAV 模式 ​

当当前场景选择 wav profile 时, runtime 会调用:

text
POST {tts.profiles.wav.endpoint}
Content-Type: application/json

请求体:

json
{
  "text": "欢迎测试语音合成",
  "speakerId": 0,
  "speed": 1.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.toml 中的 server.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 模式 ​

当当前场景选择 stream profile 时, runtime 不等待完整 WAV 合成,而是调用:

text
POST {tts.profiles.stream.endpoint}
Content-Type: application/json

请求体与 /tts 相同:

json
{
  "text": "欢迎测试语音合成",
  "speakerId": 0,
  "speed": 1.1,
  "format": "pcm"
}

callflow-esl 期望响应:

json
{
  "streamUrl": "http://127.0.0.1:9080/tts-stream/3a1c7b9eaf0c1d72.pcm",
  "streamPath": "/tts-stream/3a1c7b9eaf0c1d72.pcm",
  "sampleRate": 16000,
  "cached": false
}

必需字段:

  • streamUrl:不能为空,会被转换成 FreeSWITCH playback 目标

诊断字段:

  • streamPath:只写入日志,便于关联 TTS 服务端请求
  • sampleRate:写入日志;使用 audio_fork:// 时也用于播放目标中的 rate 参数
  • cached:服务端可能返回,但当前 callflow-esl 不依赖该字段

当 profiles.stream.playbackPrefix 配置为 "audio_fork://" 时(强力推荐,原生 HTTP Chunked PCM16 流平替 mod_shout):

  • 请求端自动协商:callflow-esl 在调用 POST /tts-stream 时会自动传入 {"format": "pcm"},请求 sherpa-tts-server 输出原生裸 PCM16 HTTP Chunked 流链接(如 /tts-stream/abc.pcm);
  • 零编解码与极速起播:服务端直接在内存中将 TTS 浮点音频量化为 PCM16 输出,彻底消除 ffmpeg 进程拉起与 MP3 压缩/解压延迟,首包延迟进一步压降 200~300ms,CPU 消耗降为零;
  • 播放目标自动组装:runtime 自动组装目标字符串为 audio_fork://${streamUrl}?rate=${sampleRate}&channels=1;
  • 底层安全驱动:FreeSWITCH 通过 mod_audio_fork 原生 SWITCH_FILE_INTERFACE 文件驱动播放 PCM16 流,安全打断且无阻塞,服务端零 ffmpeg 依赖、无需 MP3 编解码。

配置 profiles.stream.playbackPrefix="shout://" 时(MP3 模式):

  • 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。如果配置成其他非空前缀,则按字符串前缀拼接。

流式模式依赖:

  • 使用 audio_fork:// 前缀时:FreeSWITCH 需加载 mod_audio_fork,并能访问 streamUrl;sherpa-tts-server 零 ffmpeg 依赖;
  • 使用 shout:// 前缀时:FreeSWITCH 需加载 mod_shout,sherpa-tts-server 需要能找到 ffmpeg。

sherpa-tts-server 侧要求 ​

TTS 服务的监听地址、编码器和预缓冲配置以 sherpa-tts-server README 及其 config.toml 为准。server.publicBaseUrl 必须是 FreeSWITCH 可访问的地址。

POST /tts 生成 WAV 并返回播放 URL;POST /tts-stream 创建流式链接,FreeSWITCH 随后拉取 .pcm 或 .mp3。使用 audio_fork:// 时需加载 mod_audio_fork;使用 shout:// 时需加载 mod_shout,TTS 服务还需可用的 ffmpeg。

会议播放推荐配合 audio_fork:// 原生 PCM16 流使用 stream profile,安全打断且无阻塞;若流式驱动仍为 shout://,则会议必须使用 wav profile,避免 mod_shout 关闭流时阻塞会议混音线程。单呼场景无论何种驱动均可自由选择 stream 或 wav profile。

业务侧用法 ​

独立播报:

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 必须是正整数
  • speakerId、speed 仅影响本次请求,不会修改全局配置

日志与错误 ​

普通 /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 失败: <message>

流式 /tts-stream 成功时,写入:

text
tts_stream_link_resolved

流式 /tts-stream 失败时,写入:

text
tts_stream_link_failed

并向上抛出:

text
sherpa-http tts stream 失败: <message>

常见失败原因:

现象可能原因处理方式
<时长>ms 后超时TTS 服务未响应或合成耗时超过 requestTimeoutMs确认服务健康、调大 requestTimeoutMs、缩短单次文本
HTTP 400 Bad Request请求体不合法,例如文本为空或 speed <= 0检查业务传入的 text、speed
HTTP 503 Service UnavailableTTS 服务端队列已满降低并发或调大 maxQueuedRequests / ttsPoolSize
响应缺少 wavUrl/tts 响应缺少 wavUrl检查 TTS 服务端响应结构
响应缺少 fileName/tts 响应缺少 fileName检查 TTS 服务端响应结构
stream 响应缺少 streamUrl/tts-stream 响应缺少 streamUrl检查流式接口响应结构
FreeSWITCH 播放 HTTP WAV 失败publicBaseUrl 对 FreeSWITCH 不可达改成 FreeSWITCH 可访问的 IP/域名
FreeSWITCH 播放流式 MP3 失败未启用 mod_shout 或 ffmpeg 不可用启用 mod_shout,确认 TTS 服务端 [mp3].encoderPath

联调检查清单 ​

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

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