1. 项目概述:用开源技术栈构建一个轻量级IVR系统
如果你正在寻找一个成本可控、高度可定制且能快速上线的互动式语音应答(IVR)解决方案,那么将FreeSWITCH(FS)、Lua脚本和文本转语音(TTS)引擎组合起来,无疑是一条极具吸引力的技术路径。这不仅仅是把几个开源组件拼在一起,而是构建一个从电话呼入、逻辑处理到语音播报完全自主可控的通信核心。我曾在多个中小型客服、查询和通知类项目中采用这套方案,它完美地平衡了灵活性、性能和开发效率。
简单来说,这个项目就是利用FreeSWITCH作为电话交换和媒体处理的核心大脑,用Lua脚本编写灵活的业务逻辑(比如判断用户按键、查询数据库),再通过TTS引擎将文本动态转换为语音播报给来电者,从而形成一个完整的自动化语音服务系统。相比动辄数十万的商业IVR产品或复杂的云服务API,这套方案部署在你自己的服务器上,所有数据都在本地,一次投入,长期使用,特别适合对定制化要求高、有数据安全顾虑或希望深度理解系统内部运作的团队。
2. 核心组件选型与架构设计
2.1 为什么是FreeSWITCH + Lua + TTS?
在构建IVR系统时,选型决定了项目的天花板和地基。我选择这个组合,是基于多年实战中总结出的几个核心考量:
FreeSWITCH(FS)作为通信基石:它是一个功能强大的开源软交换平台,核心价值在于其稳定性和灵活性。FS不仅支持SIP协议轻松对接运营商的电话线路或SIP中继,其内置的mod_dptools模块提供了丰富的IVR相关功能,比如play_and_get_digits(播放语音并收集DTMF按键)、read(语音识别输入)等。更重要的是,它原生支持通过ESL(Event Socket Library)或嵌入式脚本(如Lua)进行控制,让我们可以用外部程序逻辑来“指挥”通话流程,这是实现复杂IVR的关键。
Lua脚本作为逻辑控制器:为什么不用更流行的Python或Node.js?在FS的嵌入式场景下,Lua有不可替代的优势。首先,FS内置了Lua解释器(mod_lua),脚本可以直接在FS进程中执行,无需进程间通信,延迟极低,性能出色。其次,Lua语法简洁,嵌入性好,特别适合编写像IVR菜单这种以状态判断和流程控制为主的逻辑。你可以把一个复杂的IVR树状菜单,写成一个个清晰的Lua函数和条件判断,维护起来直观得多。最后,它的资源占用极小,对于高并发通话场景非常友好。
TTS引擎作为声音出口:TTS的选择直接关系到用户体验。这里有两个主流方向:
- 离线TTS引擎:如eSpeak、Festival或基于深度学习的VITS、Sherpa-TTS等。优势是无需网络、延迟稳定、数据完全本地化。例如,
esp32p4 tts、kokoro离线tts等关键词反映了在嵌入式或离线环境下TTS的需求。对于播报固定内容(如欢迎词、菜单选项)或对实时性要求极高的场景,离线引擎是首选。 - 在线TTS服务:如微软Azure、谷歌Cloud TTS或国内大厂的语音合成服务。优势是音质自然、富有情感,接近真人发音。
android 预置 google tts 语音包、qwen tts v100等热词体现了对高质量语音的追求。适合对播报音质要求高,且内容动态生成(如播报查询到的账户余额、订单状态)的场景。
在我的架构里,通常将两者结合:固定提示音使用高质量离线或预录制音频以保证体验;动态内容则通过调用在线TTS API生成音频文件,再由FS播放。FreeSWITCH可以很好地管理这些音频资源。
2.2 系统架构与数据流设计
一个清晰的架构是项目成功的起点。下图描绘了典型请求的数据流:
来电者 --> (PSTN/SIP) --> FreeSWITCH --> 触发Lua IVR脚本 | v TTS引擎 (离线/在线) | v (音频文件) FreeSWITCH播放音频 | v (DTMF/语音输入) Lua脚本逻辑处理 | v (查询/计算) 数据库/外部API | v TTS生成响应音频 --> 播放给来电者核心设计要点:
- 状态无状态化设计:虽然IVR是一个有状态的会话,但我们应该尽量让Lua脚本逻辑保持清晰。利用FreeSWITCH的
session对象存储临时变量(如用户输入的账号、菜单层级),避免使用全局变量,这样系统更健壮,也便于调试。 - 音频缓存策略:对于频繁播报的固定文本(如“请输入您的账号,以井号键结束”),应预先通过TTS生成音频文件(如.wav格式),并存储在FS的音频目录中直接播放,避免每次请求都实时合成,极大提升响应速度和降低TTS服务负载。
- 错误处理与超时:必须在Lua脚本中为每一步用户输入设置合理的超时(
timeout)和最大重试次数。当用户长时间无响应或输入错误时,应能优雅地播放提示音并重新引导或转接人工坐席。
注意:在涉及
tts语音包一键导入这类操作时,务必确认音频文件的格式(如采样率、位深、编码)与FreeSWITCH兼容。通常8000Hz或16000Hz采样率的单声道PCM或G.711格式兼容性最好。使用sox或ffmpeg工具进行批量转换是标准做法。
3. FreeSWITCH基础环境搭建与配置
3.1 FreeSWITCH的安装与基础配置
部署是第一步,也是最容易踩坑的一步。我推荐从源码编译安装,以便获得最大的灵活性和对模块的控制。
# 在Ubuntu/Debian系统上的示例步骤 # 1. 安装依赖 sudo apt-get update && sudo apt-get install -y \ git build-essential autoconf automake libtool pkg-config \ libssl-dev libpcre3-dev libedit-dev libsqlite3-dev \ libcurl4-openssl-dev libopus-dev libsndfile1-dev # 2. 下载源码(以稳定版为例) git clone https://github.com/signalwire/freeswitch.git cd freeswitch git checkout v1.10.10 # 3. 编译配置,启用必要的模块 ./bootstrap.sh -j ./configure --enable-core-pgsql-support --enable-core-odbc-support \ --enable-portable-binary --prefix=/usr/local/freeswitch # 4. 编译并安装 make -j$(nproc) sudo make install安装完成后,关键一步是配置/usr/local/freeswitch/conf/autoload_configs/modules.conf.xml文件,确保以下模块被加载:
mod_lua:用于执行Lua脚本。mod_tts_commandline:这是一个非常有用的模块,它允许FS通过调用系统命令来执行TTS。mod_dptools:提供IVR核心应用。mod_sofia:SIP协议栈,用于接打电话。mod_curl:如果你计划使用在线TTS API,这个模块会很有用。
在配置文件中找到对应行,取消注释或添加:
<load module="mod_lua"/> <load module="mod_tts_commandline"/>3.2 拨号计划(Dialplan)配置:IVR的入口
拨号计划是FS的路由规则,它决定了来电应该由哪个脚本来处理。我们需要在/usr/local/freeswitch/conf/dialplan/default.xml中定义一个IVR入口。
<context name="public"> <extension name="ivr_entry"> <condition field="destination_number" expression="^8888$"> <!-- 假设用户拨打8888进入IVR --> <action application="lua" data="ivr_main.lua"/> </condition> </extension> </context>这段配置的意思是:当有电话呼入到号码8888时,FreeSWITCH会执行scripts/目录下的ivr_main.lua脚本。所有IVR的逻辑都将从这个Lua脚本开始。
实操心得:建议为测试环境配置一个简单的分机,直接互打测试IVR,避免一开始就对接复杂的外线。可以使用FS自带的
sofiaprofile和user目录下的分机配置。
4. Lua脚本编写:IVR业务逻辑的核心
4.1 Lua脚本基础结构与会话控制
Lua脚本是IVR的大脑。一个基本的IVR脚本结构如下:
-- ivr_main.lua session = freeswitch.Session() -- 获取当前通话会话对象 session:answer() -- 接听电话 -- 设置一些会话变量,如超时时间、终止键 session:setVariable("tts_engine", "flite") -- 指定TTS引擎 session:setVariable("tts_voice", "kal") -- 指定发音人 -- 播放欢迎语 session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/ivr/8000/ivr-welcome_to_freeswitch.wav") -- 进入主菜单函数 main_menu(session) -- 函数定义:主菜单 function main_menu(session) local digits = session:playAndGetDigits(1, 1, 3, 5000, "#", "/usr/local/freeswitch/sounds/en/us/callie/ivr/8000/ivr-please_enter_extension.wav", "/usr/local/freeswitch/sounds/en/us/callie/ivr/8000/ivr-that_was_an_invalid_entry.wav", "\\d+") if digits == "1" then -- 处理选项1:账户查询 account_inquiry(session) elseif digits == "2" then -- 处理选项2:业务办理 handle_business(session) elseif digits == "3" then -- 处理选项3:转人工 transfer_to_agent(session) else -- 无效输入,重试或结束 session:streamFile("/usr/local/freeswitch/sounds/en/us/callie/ivr/8000/ivr-goodbye.wav") session:hangup() end endsession:playAndGetDigits是IVR的瑞士军刀,它播放提示音,并等待用户输入指定长度的DTMF按键。参数依次为:最小位数、最大位数、最大重试次数、超时毫秒数、终止键、提示音文件、错误提示音文件、合法输入的正则表达式。
4.2 实现动态TTS播报
静态音频文件无法满足动态内容播报的需求。这里介绍两种集成TTS的方法:
方法一:使用mod_tts_commandline模块(推荐用于离线TTS)首先,在/usr/local/freeswitch/conf/autoload_configs/tts_commandline.conf.xml中配置TTS命令。例如,使用flite(一个轻量级离线TTS):
<param name="command" value="flite -t ${tts_text} -o ${tts_file}"/> <param name="voice" value="kal"/> <param name="rate" value="1.0"/>然后在Lua脚本中调用:
function play_tts(session, text) -- 设置要合成的文本 session:setVariable("tts_text", text) -- 执行TTS合成并播放,引擎使用配置文件中定义的`flite` session:execute("tts_commandline", "flite") end -- 使用示例:播报动态余额 play_tts(session, "您的账户余额是,一百二十三元五角。")方法二:调用在线TTS API(用于高质量语音)对于在线API,我们通常在Lua中通过os.execute调用curl或使用luasocket库发起HTTP请求,获取音频文件后让FS播放。
function play_online_tts(session, text, api_key) local temp_file = "/tmp/tts_" .. session:get_uuid() .. ".wav" -- 使用curl调用在线API(示例,需替换为真实API) local cmd = string.format('curl -X POST -H "Authorization: Bearer %s" -H "Content-Type: application/json" -d \'{"text":"%s"}\' "https://api.tts.service/synthesize" -o %s', api_key, text, temp_file) os.execute(cmd) -- 检查文件是否存在并播放 local f=io.open(temp_file,"r") if f~=nil then io.close(f) session:streamFile(temp_file) os.remove(temp_file) -- 播放后清理临时文件 else session:streamFile("/path/to/error_prompt.wav") end end注意事项:在线API调用涉及网络延迟,必须设置合理的超时机制,并准备好降级方案(例如,播报“系统繁忙,请稍后再试”的静态音频)。同时,临时文件管理很重要,避免磁盘被占满。可以使用会话UUID作为文件名的一部分以确保唯一性。
4.3 复杂IVR流程与状态管理
对于多层级菜单(例如:按1进入查询,再按1查余额,按2查账单),需要良好的状态管理。
function account_inquiry(session) local digits = session:playAndGetDigits(1, 1, 3, 5000, "#", "请选择,查余额请按1,查账单请按2,返回上级请按星号键。", "输入无效。", "[12\\*]") if digits == "1" then -- 获取账户信息(例如从数据库) local account_num = get_account_from_session(session) -- 假设之前已获取 local balance = query_balance_from_db(account_num) play_tts(session, "您的余额为" .. balance .. "元。") main_menu(session) -- 返回主菜单 elseif digits == "2" then handle_bill_inquiry(session) elseif digits == "*" then main_menu(session) -- 返回主菜单 else session:hangup() end end -- 一个简单的会话存储示例(实际应用中可能用数据库或KV存储) function get_account_from_session(session) local acct = session:getVariable("account_number") if not acct then acct = collect_account_number(session) -- 引导用户输入账号 session:setVariable("account_number", acct) end return acct end通过session:setVariable和session:getVariable,可以在同一个通话的整个生命周期内传递和保存数据,这是实现复杂交互的基础。
5. TTS引擎的集成与优化实战
5.1 离线TTS引擎选型与集成
对于资源受限或对延迟敏感的环境,离线TTS是必选项。除了上面提到的flite,还有更多选择:
- eSpeak:非常轻量,支持多种语言,但音质机械感较强。集成方式与
flite类似,通过tts_commandline配置命令。 - Festival:更老牌,音质和自然度相对好一些,但配置更复杂。
- 基于深度学习的离线引擎(如VITS、Sherpa-TTS):这是当前的热点。
voxsherpa tts下载、基于“sherpa-onnx 底座 + paraformer(stt) + vits (tts)”等热词指向了这个方向。这些引擎通常提供Python接口或命令行工具。集成策略是:编写一个Python脚本,接收文本参数,调用引擎合成语音并保存为文件,然后在tts_commandline配置中调用这个Python脚本。
<!-- 配置VITS类TTS引擎的示例 --> <param name="command" value="python3 /path/to/your_tts_script.py --text "${tts_text}" --output "${tts_file}""/>你的Python脚本(your_tts_script.py)需要封装模型加载和推理过程。虽然首次加载模型可能较慢,但合成单个句子速度很快。
5.2 在线TTS API的高效使用策略
使用在线API时,成本和性能是需要权衡的核心。
缓存策略:这是最重要的优化手段。建立一个简单的缓存机制,将合成过的文本(MD5哈希值作为键)和对应的音频文件路径存储起来(可以用Redis或本地文件索引)。下次遇到相同文本请求时,直接返回缓存文件,无需再次调用API。
function get_tts_audio_with_cache(text, api_key) local hash = md5(text) -- 需要引入MD5计算函数 local cache_file = "/tts_cache/" .. hash .. ".wav" if file_exists(cache_file) then return cache_file else -- 调用API合成,保存到cache_file synthesize_and_save(text, api_key, cache_file) return cache_file end end批量合成:对于已知的、可能被播报的动态文本(如“您的验证码是XXXX”),可以在系统空闲时批量预合成一批音频文件备用。
异步处理:对于非实时性要求极高的场景,可以考虑异步合成。当需要播放一段TTS时,先播放一个“请稍等”的提示音,然后在后台线程调用API,合成完成后再插入到通话中播放。这需要更复杂的会话管理和媒体控制。
5.3 音质与参数调优
TTS播报的清晰度和自然度直接影响用户体验。
- 采样率与格式:确保TTS输出的音频格式与FreeSWITCH兼容。通常16kHz、16bit、单声道的WAV(PCM)格式是通用选择。在线API返回的可能是MP3或Ogg,需要使用
sox或ffmpeg在播放前进行转码。# 使用ffmpeg将MP3转换为FS友好的WAV格式 ffmpeg -i input.mp3 -ar 16000 -ac 1 -sample_fmt s16 output.wav - 语速与音量:可以通过FreeSWITCH的
playback相关参数或TTS引擎自身的参数调整。例如在session:streamFile后使用session:execute("volume", "+3")微调音量。更精细的控制需要在TTS合成阶段就调整好语速参数。 - 静音修剪:TTS生成的音频开头和结尾可能有不必要的静音段,可以使用
sox工具进行修剪,使播报更紧凑。sox input.wav output.wav silence 1 0.1 1% reverse silence 1 0.1 1% reverse
6. 高级功能实现与系统集成
6.1 语音识别(ASR)集成实现智能IVR
纯按键(DTMF)IVR正在向语音识别(ASR)IVR演进。集成ASR可以让用户直接说话操作。FreeSWITCH的mod_dptools中的detect_speech或play_and_detect_speech应用可以配合外部ASR引擎(如Google Cloud Speech-to-Text, 阿里云ASR等)工作。
更现代的做法是使用mod_unimrcp。MRCP(Media Resource Control Protocol)是专门用于控制ASR、TTS等媒体资源的协议。你可以部署一个MRCP服务器(如Kaldi+GStreamer,或商业方案),FreeSWITCH通过mod_unimrcp与其通信。
在Lua脚本中,使用方式与playAndGetDigits类似,但获取的是文本:
-- 这是一个概念性示例,实际配置复杂 session:execute("play_and_detect_speech", "say:请说出您的需求, grammar:builtin:grammar/boolean?language=zh-CN;") local asr_result = session:getVariable("detect_speech_result") if asr_result then -- 解析asr_result,例如包含“余额”、“账单”等关键词 if string.find(asr_result, "余额") then account_inquiry(session) end end集成ASR会显著增加系统复杂性,需要处理识别错误、网络超时、方言和噪音等问题。初期建议从简单的、限定领域的语法(Grammar)开始。
6.2 与后端业务系统对接
IVR的核心价值在于与业务数据联动。Lua脚本可以通过多种方式与后端交互:
数据库直连:使用Lua的数据库连接库(如
luasql.mysql)直接查询。这种方式简单直接,但将业务逻辑与数据库耦合,且数据库连接管理(如连接池)需要在Lua中自己实现,不推荐用于高并发生产环境。local luasql = require "luasql.mysql" local env = luasql.mysql() local conn = env:connect('database_name', 'username', 'password', 'localhost', 3306) local cursor = conn:execute("SELECT balance FROM accounts WHERE id = " .. account_id) local row = cursor:fetch({}, "a") if row then balance = row.balance end cursor:close() conn:close() env:close()HTTP/API调用(推荐):这是更解耦、更灵活的方式。Lua脚本通过
curl命令或luasocket.http模块调用内部RESTful API。后端服务可以是用任何语言(Java, Go, Python)编写的微服务。local http = require "socket.http" local ltn12 = require "ltn12" local response_body = {} local res, code = http.request{ url = "http://internal-api:8080/account/balance?acct=" .. account_num, sink = ltn12.sink.table(response_body) } if code == 200 then local balance = table.concat(response_body) -- 处理balance end这种方式便于权限控制、日志记录和业务逻辑升级。
消息队列:对于耗时较长的操作(如生成复杂报告),IVR可以将任务放入消息队列(如Redis List, RabbitMQ),并告知用户“请求已受理,稍后通知您”。由后台Worker处理完成后,通过短信或回拨电话通知用户。
6.3 通话录音、日志与监控
一个可运维的IVR系统离不开完善的日志和监控。
- 通话录音:在Dialplan或Lua脚本中,使用
session:execute("record_session", "/path/to/recordings/${uuid}.wav")可以轻松开启录音。录音文件用于质检和纠纷处理。 - 详细日志:在Lua脚本中关键节点使用
freeswitch.consoleLog("INFO", "IVR: User entered main menu.\n")输出日志。同时,配置FreeSWITCH的logfile.conf.xml,将不同级别的日志输出到不同文件。 - 性能监控:监控FreeSWITCH进程的CPU、内存占用,以及当前通话数量(
show calls命令)。可以使用mod_event_socket将关键事件(如呼叫开始、结束、IVR菜单选择)推送到外部监控系统(如Prometheus+Grafana),绘制实时流量和菜单选择热力图。
7. 部署、调优与故障排查实录
7.1 系统部署与资源规划
部署环境的选择取决于预期并发量。
- 低并发(<10路同时通话):一台2核4GB的云服务器或虚拟机足以胜任,运行FreeSWITCH和轻量级TTS(如flite)。
- 中高并发(10-100路):需要更专业的规划。建议将FreeSWITCH、TTS服务(尤其是深度学习TTS)、数据库/API后端部署在不同的服务器上。FreeSWITCH本身对CPU(媒体编解码)和网络I/O敏感,应选择高主频CPU和低延迟网络。
- 关键配置:
- Ultrasound:调整
/usr/local/freeswitch/conf/autoload_configs/switch.conf.xml中的max-sessions参数,设置最大并发会话数。 - 网络:确保服务器开启必要的UDP端口(默认为5060 SIP, 16384-32768 RTP端口范围)并配置好防火墙。
- 音频文件:将所有提示音、TTS缓存文件放在高性能存储(如SSD)上,减少播放延迟。
- Ultrasound:调整
7.2 性能调优实战经验
FreeSWITCH调优:
- 禁用不必要的模块:在
modules.conf.xml中只加载需要的模块,减少内存占用和潜在冲突。 - RTP端口范围:在
sofia.conf.xml中合理设置RTP端口范围,避免与其它服务冲突。 - 日志级别:生产环境将日志级别调整为
INFO或WARNING,避免DEBUG级别产生大量日志拖慢性能。 - 使用
hiredis:如果大量使用Redis做缓存或会话存储,编译时启用mod_hiredis,它比通用的mod_redis性能更好。
- 禁用不必要的模块:在
Lua脚本优化:
- 避免阻塞操作:在Lua脚本中,像
os.execute调用耗时命令、同步HTTP请求都是阻塞的,会卡住整个通话线程。对于可能耗时的操作(如调用慢速API),应考虑使用FreeSWITCH的bgapi(后台API)在非阻塞线程中执行。 - 预加载常用库和函数:确保Lua脚本开头只
require必要的库。复杂的函数可以预编译。 - 会话变量管理:不要滥用
session:setVariable存储大量数据。只存储必要的信息。
- 避免阻塞操作:在Lua脚本中,像
TTS性能瓶颈:
- 预热:对于深度学习TTS模型,在服务启动后先用一些典型文本进行“预热”推理,避免第一次合成时速度慢。
- 并发限制:离线TTS引擎,特别是大型神经网络模型,对GPU/CPU资源消耗大。需要限制同时合成的任务数,可以通过简单的任务队列实现。
7.3 常见问题与排查技巧
以下是我在项目中反复遇到的一些典型问题及解决方法:
| 问题现象 | 可能原因 | 排查步骤与解决方案 |
|---|---|---|
| 电话能接通,但听不到任何声音 | 1. 音频文件路径错误或格式不支持。 2. RTP音频流未建立(防火墙/NAT问题)。 3. FreeSWITCH音频设备未正确配置。 | 1. 在FS CLI用file /path/to/file.wav测试文件能否播放。用sox或ffmpeg检查音频格式(采样率、编码)。2. 在FS CLI用 sofia status profile internal查看SIP注册状态,用show channels查看通话的RTP状态。检查服务器防火墙和云服务商安全组规则,确保RTP端口范围开放UDP。3. 检查 switch.conf.xml中rtp-start-port和rtp-end-port设置。 |
| Lua脚本不执行或报错 | 1. 脚本语法错误。 2. mod_lua模块未加载。3. 脚本文件权限问题。 4. Lua路径问题。 | 1. 使用luac -p your_script.lua检查语法。在脚本开头加freeswitch.consoleLog("INFO", "Script started\n")看日志。2. 在FS CLI执行 show modules,确认mod_lua状态为LOADED。3. 确保脚本文件对FreeSWITCH运行用户(通常是 freeswitch)有读取权限。4. 检查 /usr/local/freeswitch/conf/autoload_configs/lua.conf.xml中script-directory配置的路径。 |
| TTS合成失败或播放异常 | 1. TTS命令配置错误。 2. 磁盘空间不足。 3. 在线TTS API调用失败(网络、鉴权、额度)。 4. 生成的音频格式FS无法播放。 | 1. 直接在命令行手动执行tts_commandline配置中的命令,看是否能成功生成文件。2. 检查 df -h。3. 查看Lua脚本中调用API的返回码和错误信息。检查API密钥是否过期、是否有调用频率限制。 4. 用 soxi或ffprobe检查TTS生成文件的详细格式,确保与FS兼容。在tts_commandline配置中,可以添加格式转换命令管道。 |
| 用户按键无法识别 | 1.playAndGetDigits参数设置不当(如超时太短)。2. 电话机或网关DTMF格式不标准(RFC2833 vs. Inband)。 | 1. 增加timeout和tries参数值。检查terminators(终止键)设置是否正确。2. 在FS CLI使用 sofia global siptrace on开启SIP信令跟踪,查看DTMF是通过SIP INFO(RFC2833)还是RTP载荷(Telephone-Event)发送的。在SIP profile配置中确保正确支持了这两种格式。 |
| 高并发下系统不稳定或崩溃 | 1. 服务器资源(CPU、内存、端口)耗尽。 2. Lua脚本或TTS引擎存在资源泄漏。 3. 数据库或后端API连接池耗尽。 | 1. 使用top、htop监控资源。调整max-sessions限制最大并发。检查ulimit -n确保文件描述符足够。2. 检查Lua脚本中数据库连接、文件句柄是否及时关闭。对于离线TTS,监控其进程是否存在内存累积。 3. 对后端服务进行压力测试,并实施熔断、降级策略。在Lua脚本中增加调用超时和重试逻辑。 |
一个具体的排查案例:曾遇到用户反映“按了键没反应”。通过查看FreeSWITCH日志(tail -f /usr/local/freeswitch/log/freeswitch.log),发现大量PLAY_AND_GET_DIGITS动作因超时而结束。进一步检查,发现提示音文件是一个48kHz采样率的MP3,FreeSWITCH在播放前需要进行转码,导致播放前的静默时间过长,用户还没来得及听清提示,第一个输入超时就已经开始了。解决方案是将所有提示音文件预先转换为8kHz或16kHz的PCM WAV格式,问题立即解决。
构建一个稳定、高效的FS+Lua+TTS IVR系统,是一个将通信技术、软件开发和运维知识相结合的过程。从简单的菜单导航开始,逐步集成动态TTS、后端数据查询,甚至语音识别,你会对“电话背后的技术”有更深刻的理解。这套开源方案给了你从底层控制一切的能力,虽然需要自己处理更多细节,但带来的灵活性、成本优势和知识积累是商业方案无法比拟的。最重要的是,在每次解决一个像音频格式不兼容或API调用超时这样的具体问题后,整个系统的稳健性就增加一分,这种掌控感正是技术人的乐趣所在。