简介:面向FreeSWITCH开发者的中文语音合成放音模块,基于阿里文本转语音技术,为呼叫中心快速增加文本朗读能力。模块支持在线合成与缓存播放两种调用方式:前者直接指定需要朗读的文字,后者引用一个已合成的语音名称,并可通过管理接口随时添加新的预合成语音,适合交互式语音应答、来电通知、验证码播报等场景,也能嵌入原有拨号方案。压缩包共有4个文件:可直接部署的动态库模块、配置文件模板、文字版使用说明和用于二次编译的源码压缩包,整体仅7.57兆字节。动态库放入模块目录即可识别,配置模板可快速完成鉴权参数调整,使用文档给出完整的调用示例与注意事项,源码包则便于中高级用户自行修改。已有554人学习浏览,适合具备一定FreeSWITCH基础、希望在项目中接入中文语音能力的运维或通信开发人员。通过该包,使用者既能获得开箱即用的放音能力,也能掌握缓存语音的维护思路,为后续扩展与性能调优打下基础。
1. mod_ali_tts 究竟是什么:一个让 FreeSWITCH 说中文的 TTS 放音模块
FreeSWITCH 的放音机制很成熟,但原生 TTS 合成出来的中文,听着像机器人在念稿,多音字和停顿基本靠猜。阿里 TTS 的合成效果明显好一截,mod_ali_tts 这个模块就是把这套云端语音合成能力接进 FreeSWITCH 的一座桥。装上它以后,dialplan 里写一句 TTS 放音规则,文字过去,合成音频流自动回传播放。这份资源解决的核心痛点是:原生 TTS 效果差、本地引擎调不动时,怎么快速把阿里 TTS 变成自己的放音引擎。适合做 IVR、语音通知、外呼机器人的开发同学。
2. 从原理到选型:FreeSWITCH 原生 TTS 的短板与阿里 TTS 的补位
2.1 speak 接口的调用链:模块注册与音频回传
FreeSWITCH 中实现 TTS 放音,本质上是往 speak 引擎列表里塞模块。FreeSWITCH 启动时扫描 mod 目录下的模块,每个模块通过 module_interface_t 结构把自己注册为 speak 引擎。dialplan 里执行 speak 应用时,FreeSWITCH 根据 data 参数的第一段去找对应的引擎名,然后把文本内容和调用参数打包,调用模块注册时填写的回调函数。
mod_ali_tts 的完整工作链路是这样的:
- speak 应用被触发,文本进入模块回调
- 模块读取配置文件中的全局参数(voice、sample_rate 等)
- 组装 HTTP 请求,带上鉴权信息
- 请求阿里 TTS 的 REST 接口
- 收到音频流转成 wav/pcm
- 按当前通道的采样率做重采样,交给媒体通道播放
第 6 步是最容易出问题的环节。阿里 TTS 返回的音频采样率取决于请求参数,常见是 16000 或者 24000,而 FreeSWITCH 默认通话通道采样率只有 8000。采样率不匹配的情况下,即使强制放音,用户听到的也是语速异常或者直接无声。模块内部一般会做一次重采样,把音频数据对齐到通道的 native rate,这一步做得不干净,放出来就会有杂音或者音调问题。
模块注册的代码逻辑,核心结构通常长这样:
static switch_speak_interface_t ali_tts_interface = { .name = "ali_tts", .speak = ali_tts_speak_callback, .shutdown = ali_tts_shutdown_callback }; SWITCH_MODULE_LOAD_FUNCTION(mod_ali_tts_load) { switch_speak_add_interface(&ali_tts_interface); return SWITCH_STATUS_SUCCESS; }.name 是引擎名,对应 dialplan 里 speak data 参数的第一个字段;.speak 是真正的合成回调,处理从文本到音频的完整逻辑。整个模块的核心就是把这个回调写对。speak 回调里不能做阻塞太久的操作,比如同步等待网络响应,因为 speak 在单路通话里是串行的,阻塞久了会直接影响这一路通话的后续动作。
2.2 为什么选阿里 TTS:本地引擎的差距与兜底策略
早期项目常用 mod_tts_command 搭配 flite 或 pico2wave 这类本地引擎。好处是零网络依赖、部署快,在纯内网环境的语音提示里能用。但中文合成效果确实不行,多音字和数字读法经常翻车,客户一听就知道是机器。云端 TTS 的核心优势在于自然度和可调性。
| 对比项 | 本地轻量 TTS | 阿里 TTS |
|---|---|---|
| 中文多音字 | 错读靠运气 | 上下文消歧 |
| 数字/金额读法 | 基本按字读 | 金额、电话号语义处理 |
| 音色 | 单一且机械 | 多种人声可选 |
| 并发扩展 | 受本机 CPU 限制 | 云端按 QPS 扩容 |
| 网络依赖 | 无 | 需要稳定的出口链路 |
选型时我主要看两个指标:合成自然度和接口稳定。自然度决定了客户愿不愿意听下去,接口稳定决定了会不会半夜收到告警。阿里 TTS 的问题不是效果,而是网络依赖,所以设计上要留好兜底路径。常见做法是模块内部设置超时,超时后降级到本地播放一段预录的"系统繁忙"提示音,体验打折,但呼叫流程不会断掉。
2.3 资源包里有什么:三块内容讲透
mod_ali_tts.zip 解压之后,核心内容非常克制,三块:模块源码、配置模板、说明文档。源码主要是 mod_ali_tts.c,实现了 speak 接口回调,函数集中在 2000 行以内,结构清楚。配置文件模板把 access_key_id、voice、sample_rate、timeout 这些参数全部写成占位符,注释里写了每个参数的取值范围。说明文档写了编译命令和部署路径。
没有多余的前端封装。正因如此,它的侵入性很小,不需要改 FreeSWITCH 核心代码,编译好复制 .so 文件进去就能加载。对于已经在线上跑着老版本 FreeSWITCH 的环境,这是最友好的接入方式。
3. 编译与加载:把 mod_ali_tts 装进 FreeSWITCH 的完整步骤
3.1 依赖准备:libcurl 与 libxml2 缺一不可
动手之前先把环境确认一遍。mod_ali_tts 的 HTTP 请求走 libcurl,XML 解析走 libxml2,这两个库的 dev 包必须装好。
# Debian/Ubuntu 系列 apt-get install -y libcurl4-openssl-dev libxml2-dev # CentOS/RHEL 系列 yum install -y libcurl-devel libxml2-devel装完之后用 pkg-config 检查版本,能正常输出版本号才算环境就绪:
pkg-config --modversion libcurl libxml-2.0一个常见的翻车点是:系统里装的是 libcurl 运行时库,但没装 -dev 包,编译时找不到头文件。这个检查能帮你把编译期和运行期的坑分开,省下不少排查时间。
3.2 编译安装:make 与 make install 的完整路径
把资源包解压到 FreeSWITCH 源码树的 asr 目录下,然后进入该目录直接 make。
cd /usr/local/src/freeswitch/src/mod/asr unzip /data/pkgs/mod_ali_tts.zip -d mod_ali_tts cd mod_ali_tts make make installmake 过程会调用 FreeSWITCH 的构建系统,自动链接模块需要的核心符号。make install 会把 mod_ali_tts.so 安装到 FreeSWITCH 的 modules 目录,通常是 /usr/local/freeswitch/mod。如果 make 报错找不到 freeswitch.h 一类的头文件,说明 FreeSWITCH 源码还没执行过 configure,回到源码根目录跑一次 ./configure 即可解决。
3.3 配置文件解析:每个参数对应什么行为
模块加载后会读取 conf/autoload_configs/ali_tts.conf.xml。这是全局默认配置,dialplan 调用时如果没有显式覆盖参数,就是用这里面的值。
<configuration name="ali_tts.conf" description="Ali TTS Configuration"> <settings> <param name="access_key_id" value="your_access_key_id"/> <param name="access_key_secret" value="your_access_key_secret"/> <param name="endpoint" value="https://your-gateway-address/tts"/> <param name="voice" value="xiaoyun"/> <param name="sample_rate" value="16000"/> <param name="volume" value="50"/> <param name="speech_rate" value="0"/> <param name="pitch_rate" value="0"/> <param name="format" value="wav"/> <param name="timeout" value="3000"/> </settings> </configuration>逐项说下关键参数:
- access_key_id / access_key_secret:调用语音服务接口的鉴权凭证,在云控制台申请,注意别提交到 git 仓库
- endpoint:语音服务网关地址,不同区域有不同域名,按你申请的实例填
- voice:音色标识,标准女声适合 IVR 主菜单,新闻男声适合通知播报
- sample_rate:合成音频采样率,常见 16000 或 24000。要和 FreeSWITCH 媒体通道匹配,不匹配会翻车
- volume:音量,范围 0-100。50 左右是安全值,超过 70 容易在编码时削波
- speech_rate / pitch_rate:语速和音调倍率,负值是变慢/变低,正值变快/变高
- format:返回音频容器格式,wav 带文件头,pcm 是裸流
- timeout:请求超时,单位毫秒。太短容易因网络抖动导致放音失败,太长会卡住信令流程
这里 sample_rate 和 timeout 是上线前必须验证的两个值。前者决定能不能正常放出声音,后者决定在异常网络下会不会拖死整个通话流程。我在测试环境调参时,会把 timeout 先设成 5000 观察基线,再逐步降到 3000,目的是找到网络抖动和响应延迟之间的平衡点。
3.4 加载验证:module_exists 与第一个报错
模块安装完,在 modules.conf.xml 里登记加载:
<load module="mod_ali_tts"/>重启 FreeSWITCH,在控制台验证加载状态:
freeswitch> module_exists mod_ali_tts返回 true 就说明模块注册成功。返回 false 时,先手动 load 一次看具体的错误输出:
freeswitch> load mod_ali_tts这一步能区分问题类型:如果报 cannot open shared object file,那就是动态库路径问题;如果报 undefined symbol,那大概率是编译时 FreeSWITCH 版本和当前运行版本不一致,需要重新对着当前版本的源码编译模块。
4. Dialplan 调用实战:放音、打断与参数调优
4.1 最小放音规则:从接听到播报
模块就位后,dialplan 里最简单的调用长这样:
<extension name="ivr_welcome"> <condition field="destination_number" expression="^1000$"> <action application="answer"/> <action application="sleep" data="500"/> <action application="speak" data="ali_tts|您好,欢迎致电某某服务中心,请按提示音操作。"/> <action application="hangup"/> </condition> </extension>执行流程是:呼入走到这个 extension,answer 建立通话,sleep 500 毫秒等媒体通道就绪,speak 把文本发给 ali_tts 引擎,放完挂机。speak 的 data 参数格式是"引擎名|文本内容",引擎名必须和模块注册的 .name 字段一致,否则会报找不到 speak 引擎。
sleep 的作用经常被忽略。很多话机在 answer 之后立刻收到的 RTP 包会被丢弃,导致播报开头几个字丢失。加一个 300 到 500 毫秒的延时能有效规避这类丢字问题,特别是在 NAT 穿透场景下效果更明显。
4.2 按次参数覆盖:一条规则切换音色和语速
全局配置文件的参数是默认值,实际业务中经常需要按场景切换音色、语速。mod_ali_tts 支持在文本参数后面追加覆盖参数:
<action application="speak" data="ali_tts|voice=xiaoyun,speech_rate=-20|您的预约已成功,请您准时到场。"/>处理逻辑是:先用配置文件里的参数构造请求,再解析竖线后的 key=value 字符串,逐个覆盖到请求参数里。voice=xiaoyun 切换音色,speech_rate=-20 把语速调慢。这样同一段放音逻辑可以通过不同参数适应不同业务场景,不用改配置重启。
4.3 动态拼接文本:从变量到放音
真实场景下放音内容往往不是写死的。FreeSWITCH 里可以用 channel variable 拼接动态内容:
<action application="set" data="notice_text=先生/女士,您的工单编号是${order_id},请留意短信通知。"/> <action application="speak" data="ali_tts|${notice_text}"/>先把文案拼到变量里,再用 speak 引用。这种写法的好处是业务逻辑和语音播报分离,后续要改文案格式只动 set 那行。注意:文本里的中文标点会影响合成时的停顿节奏,建议用中文逗号和句号,不要用英文标点,不然合成出来的语气会显得生硬。
音色参数也可以做成变量:
<action application="set" data="tts_voice=xiaoyun"/> <action application="speak" data="ali_tts|voice=${tts_voice}|工单编号${order_id}已受理。"/>同一个 extension 在不同分支里给不同音色,工程上更好维护。
4.4 按键打断:TTS 放音被用户操作中断
IVR 场景里经常有"用户在 TTS 播报过程中按键"的需求。speak 应用本身是阻塞式的,放音过程中 dialplan 不会继续往下执行。处理打断的标准做法是配合 bind_meta_app。
<action application="bind_meta_app" data="1 bind meta1 speak 1 ali_tts|正在为您转接人工,请稍候。"/> <action application="answer"/> <action application="speak" data="ali_tts|请选择服务,按1转人工,按2查询余额。"/>用户按 1 的时候,正在执行的 speak 会被中断,转而执行 meta1 绑定的内容。这个机制在排队场景和自助服务场景里特别常用。bind_meta_app 的参数里同样支持参数覆盖,只是语法稍微不同。我一般把音色和语速放在 meta 参数里一起绑定,避免打断后出现音色突变的问题。
4.5 长文本分段放音:超过网关上限的处理
阿里 TTS 接口对单次合成文本长度一般有上限,大概几百字。长文本直接怼进去会报参数错误。我的处理习惯是在 dialplan 里先把文本按标点切分,分段调用 speak:
<action application="set" data="long_text=第一部分内容。第二部分内容,请记录。"/> <action application="speak" data="ali_tts|${long_text:0:20}"/> <action application="speak" data="ali_tts|${long_text:20}"/>用 FreeSWITCH 的变量切片语法,把长文本按字符数切成两段,逐段放音。这里要注意,切片位置最好落在标点处,否则句子中间断开,合成出来的语气很突兀。切好之后,两段之间加一个 100 毫秒的 sleep,听感上自然一些。
5. 避坑与排查:五个高频问题的现象、原因与解决
5.1 编译时报 undefined reference to curl_easy_init
现象:make 到链接阶段报大量 undefined reference 错误,错误列表里全是 curl_easy_init、curl_easy_perform 这类符号。
原因:编译环境缺少 libcurl 的头文件或链接库。最常见的是运行时环境有 libcurl,但开发包装得不对,或者 FreeSWITCH 构建系统没把 -lcurl 传给模块链接器。
解决:先验证动态库是否注册:
ldconfig -p | grep libcurl确认有 libcurl.so 之后,再检查模块的 Makefile 或 configure.ac 是否链接了 curl。没链接就手动把 -lcurl 补进 LIBS 变量,或者把 pkg-config --libs libcurl 的输出加进去,重新编译。我遇到过一次 pkg-config 正常但链接失败的场景,原因是 libcurl 的 .so 文件权限被改,手动 chmod 拍平权限后解决。
5.2 模块加载失败:libcurl.so.4 找不到
现象:module_exists 返回 false,控制台打印 error while loading shared libraries: libcurl.so.4。模块文件明明存在,就是加载不进来。
原因:运行时动态库搜索路径不包含 libcurl 的安装位置。这种情况经常出现在源码方式安装了新版 libcurl、或者自定义安装目录的环境里。
解决:把 libcurl 的库路径加入 LD_LIBRARY_PATH,或者在 FreeSWITCH 的 lib 目录下建软链。我倾向用 ldconfig 来管理,写一个 .conf 文件放到 /etc/ld.so.conf.d/ 下,然后执行 ldconfig。这样 FreeSWITCH 升级之后配置不会丢。改完 LD_LIBRARY_PATH 的方式要小心,它只对当前 shell 生效,如果 FreeSWITCH 是 systemd 启动的,需要在 service 文件里单独加 Environment 行。
5.3 speak 执行成功但用户听不到声音
现象:日志显示请求发送成功、音频流也返回了,但用户侧完全没有声音。
原因:模块返回的音频采样率和当前媒体通道不匹配。阿里 TTS 默认合成 16kHz 音频,FreeSWITCH 默认通道采样率是 8kHz,模块如果没有做重采样,音频数据丢进通道后解析失败,自然没有声音。
解决:先查全局采样率:
fs_cli -x "global_getvar sample_rate"如果返回 8000,把 ali_tts.conf.xml 里的 sample_rate 改成 8000,重载配置。更彻底的方案是把 FreeSWITCH 的媒体采样率提升到 16000,并在 SIP profile 的 codec 列表里加上 L16/16000,这样合成音频不降采样直接播放,听感提升明显。这个改动涉及全局媒体配置,影响面较大,我建议在测试环境完整验证完再上线。改完以后,一定要复查通话的 codec 协商结果,不然有些话机不支持 16kHz 会直接协商失败。
5.4 并发一上来就大量超时失败
现象:单路测试一切正常,压测到 20 路以上并发放音时,模块开始大量报 HTTP 超时,失败率飙升。
原因:模块每次请求都新建 TCP 连接,没有复用连接。语音服务网关对短连接请求有限流策略,每个新连接都要做 TLS 握手和鉴权,连接建立本身就消耗了大部分请求时间。
解决:在模块内部启用 keep-alive。用 curl 的 easy handle 时设置 CURLOPT_TCP_KEEPALIVE,或者用 curl share interface 做连接池。如果不想改代码,临时方案是热点文案缓存:夜间低峰批量预合成高频文案,存成 wav,dialplan 里优先用 playback,动态内容才走 speak。这个方法在大多数 IVR 场景能覆盖七成以上的请求量。注意,加连接池之后要观察模块的文件描述符数量,连接池不释放会导致 FD 泄漏,最后把整个进程拖垮。
5.5 播报开头丢字,录音文件却完整
现象:客户反馈每次听播报开头几个字都是断的,但抓包录音出来内容是完整的。
原因:这是通话链路问题,和 TTS 模块无关。answer 之后媒体协商还没完全稳定,话机丢弃了前几个 RTP 包。NAT 穿透不稳定的时候尤其明显。
解决:speak 前加 sleep,从 300ms 起步,逐步加大到不丢字为止。我遇到过一个项目要在 answer 后加 800ms 才稳定,排查下来是话机 NAT keep-alive 间隔和媒体端口老化时间不匹配。这个参数和话机型号强相关,同一套配置在不同型号话机上表现差异很大,上线前一定要用客户主力话机实测几轮。
6. 进阶技巧:把 TTS 开销降下来,同时保证放音质量
模块能跑通只是第一步。真正上线后,TTS 的成本、延迟、稳定性压力全部集中在单次请求上。我这里有几个处理习惯,分享出来供参考。
第一招是热点文案缓存。IVR 场景里真正高频的文案就那几十条——"欢迎致电"、"请稍候"、"人工服务请按0"。我把这些文案的合成结果做成预生成 wav,存到 /usr/local/freeswitch/tts_cache 目录,dialplan 里这么用:
<action application="playback" data="/usr/local/freeswitch/tts_cache/welcome.wav"/>playback 是 FreeSWITCH 最稳定的放音方式,不走网络,不占 HTTP 连接,延迟基本是零。speak 只留给动态内容,比如订单号、金额这类需要实时拼装的文本。
第二招是并发限流。TTS 模块发往网关的请求需要做数量控制。我在模块里加了一个计数信号量,把同时进行的合成请求限制在 10 个以内,超出的请求按顺序排队。这样即使业务瞬间涌入百路外呼,也不会一口气向网关发起上百并发请求,有效避开上游限流导致的大面积失败。这个信号量的值要根据网关的 QPS 配额来定,配少了放音排队会拉长,配多了又会触发限流,我在压测环境里用 10 这个值测过,稳定性和时延的平衡最好。
第三招是质量回归验证。我把一段包含多音字、数字、英文字母、生僻字的测试文案固定下来,每次调整参数后都合成一遍,录下来,用和客户同型号的话机实际试听。重点检查三个点:开头有没有丢字、音量是否贴近人工服务水准、长句子的停顿是否自然。这个流程看起来简单,但确实是避免"改一次参数翻一次车"的后悔药。
上次给某个外呼项目做调优,就是从那套热点缓存开始,把 TTS 调用量降了七成。从那以后我每次做放音方案,都会先问清楚:文案列表里哪些是固定不动的,先抽出来走缓存,再谈剩下动态内容怎么办。希望帮到你。
本文还有配套的精品资源,点击获取