IVR(Interactive Voice Response)即交互式语音应答,也就是我们说的电话语音菜单,可以使用预先录制的语音或者TTS进行自动应答,提供菜单导航,主要用于呼叫中心系统。我们主要介绍FreeSWITCH提供的IVR功能。
FreeSWITCH的IVR系统默认的配置文件为conf/autoload_configs/ivr.conf.xml
,它包含了conf/ivr_menus/目录下所有的XML文件,下面我们创建一个XML配置文件conf/ivr_menus/ivr.xml,内容如下:
代码语言:javascript复制<include>
<menus>
<menu name="welcome"
greet-long="ivr/welcome.wav"
greet-short="ivr/welcom_short.wav"
invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
exit-sound="voicemail/vm-goodbye.wav"
timeout="15000"
max-failures="3"
max-timeouts="3"
inter-digit-timeout="2000"
digit-len="4">
<entry action="menu-exec-app" digits="0" param="transfer 1000 XML default"/>
<entry action="menu-exec-app" digits="/^(10[01][0-9])$/" param="transfer $1 XML default"/>
</menu>
</menus>
</include>
在上述配置中,首先,我们指定菜单的名字(name)是welcome,其他各项的含义如下:
- greet-long:指定最开始的欢迎音,即为最开始播放的声音,比如“您好,欢迎致电烟台小樱桃网络科技有限公司,请直拨分机号,查号请拨0”的语音,该语音文件默认的位置应该是在/usr/local/freeswitch/sounds目录下。
- greet-short:该项指定一个简短的提示音,当用户长时间没有按键,操作超时时,再次播放的欢迎音,比如”请直拨分机号,查号请拨0“。
- invalid-sound:如果用户按错了键,则会使用该提示。如果你安装时使用了“make sounds-install”命令安装了声音文件,则该文件应该是默认存在的。
- exit-sound:该项指定最后菜单退出时的声音,默认会提示“Good Bye”。
- timeout:指定超时时间(毫秒),等待用户输入按键的最大超时时间。
- max-failures:指容忍用户按键错误的次数。当用户的按键与所有的菜单配置都不匹配,则失败。
- max-timeouts:即最大超时次数。
- inter-digit-timeout:为两次按键的最大间隔(毫秒)。如用户拨分机号1001时,假设拨了10,等5秒,然后再按01,这时系统实际收到的号码为10(后面的01超时后没有收到),则会播放invalid-sound指定的声音文件以提示错误。
- digit-len:说明菜单项的长度,即最大收号位数。在本例中,用户分机号长度为4位,因此我们使用4,等收到4位按键时,立即执行相应的动作,否则一直等直到按键超时。
可以看到ivr的动作主要是在entry项里配置完成的,在上述例子中,第一个entry里配置了按键0,通过menu-exec-app执行一个FreeSWITCH的App(transfer),再次通过Dialplan路由,将通话分配到被叫是1000的路由规则上,默认该规则是1000的分机号码。
菜单中的另一个entry的按键规则是一个正则表达式,表示匹配按键是1001~1019的输入,匹配成功后,会将按键赋值给$1,然后再次进行路由。比如用户输入了1019的按键,会通过执行transfer,将通话分配到被叫是1019的路由规则上,默认该规则是1019的分机号码。
如果来电用户按其他按键,则由于找不到匹配的菜单项进而提示错误(invalid-sound指定的声音),并提示用户重新输入。
以上菜单设定好后,需要在控制台中执行reloadxml使配置生效。
配置完成后就可以在控制台上进行如下测试(呼叫1001,接听后进入ivr菜单):
代码语言:javascript复制 freeswitch> originate user/1001 &ivr(welcome)
测试成功后,你就可以配置Dialplan把并户来话转接到菜单了,在Dialplan中加入一个extension(请注意,你需要加到正确的Dialplan Context中,如果不确定应该加到哪个Context中的话,在default和public中都加上会比较保险。):
代码语言:javascript复制<extension name="incoming_call">
<condition field="destination_number" expression="^777$">
<action application="answer" data=""/>
<action application="sleep" data="1000"/>
<action application="ivr" data="welcome"/>
</condition>
</extension>
接下来呼叫777进行测试。
通过上面的ivr.xml的配置,我们已经知道如何配置一个简单的IVR了,接下来我们配置一个带有二级菜单的IVR。
代码语言:javascript复制<include>
<menus>
<menu name="main"
greet-long="ivr/main_welcome.wav"
greet-short="ivr/main_welcome_short.wav"
invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
exit-sound="voicemail/vm-goodbye.wav"
timeout ="10000"
max-failures="3"
digit-len="4">
<entry action="menu-exit" digits="*"/>
<entry action="menu-sub" digits="2" param="sub"/>
<entry action="menu-exec-app" digits="0" param="transfer 1000 XML default"/>
</menu>
</menus>
</include>
代码语言:javascript复制<include>
<menus>
<menu name="sub"
greet-long="ivr/sub_welcome.wav"
greet-short="ivr/web_welcome_short.wav"
invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
exit-sound="voicemail/vm-goodbye.wav"
timeout="15000"
max-failures="3"
max-timeouts="3"
inter-digit-timeout="2000"
digit-len="4">
<entry action="menu-exit" digits="*"/>
<entry action="menu-back" digits="6"/>
<entry action="menu-top" digits="7"/>
<entry action="menu-exec-app" digits="/^(10[01][0-9])$/" param="transfer $1 XML default"/>
</menu>
</menus>
</include>
上面配置了两个IVR,名字分别是main
、sub
,顾名思义,main是主菜单,sub是子菜单, 下面先介绍下entry里的action:
- menu-exit:退出整个IVR菜单
- menu-sub:进入子菜单,比如上述的XML,我们将sub作为子菜单
- menu-back:返回上一级菜单
- menu-top:返回主菜单,也就是第一级菜单
- menu-exec-app:执行相应的application,比如transfer
配置了XML后,同样需要在控制台中执行reloadxml使配置生效。
配置完成后就可以在控制台上进行如下测试(依然呼叫1001,接听后进入ivr菜单):
代码语言:javascript复制freeswitch> originate user/1001 &ivr(main)
进入主菜单后,我们可以按2进入子菜单,在子菜单中如果我们可以按6返回上一级菜单,按7返回主菜单,不过由于我们只有一级子菜单,因此这里按键6和7的效果是一样的。如果读者感兴趣,可以自己配置多个子菜单来验证下menu-back和menu-top的区别。
不过我们也看到了,我们上面的XML IVR极其简单,在实际的业务中,我们可能需要和外面的一些服务做交互,比如查询数据库,请求一个Web服务,等等,因此我们需要一种更灵活的方式来配置IVR应用,在此,我们介绍下使用Lua方式实现的IVR:
FreeSWITCH的mod_lua模块支持Lua语言,由于Lua是一种嵌入式语言,可以很容易嵌入到程序中,因此使用Lua给我们带来很多便捷。最新的模块已经支持Lua 5.2。下面我们用Lua来实现一遍上面的welcomeIVR。
代码语言:javascript复制local tts_engine = "tts_commandline"
local tts_voice = "zh-CN-XiaoxiaoNeural"
session:set_tts_params(tts_engine, tts_voice)
session:setVariable("tts_engine", tts_engine)
session:setVariable("tts_voice", tts_voice)
session:answer()
session:sleep(1000)
local digits = session:playAndGetDigits(1, 4, 3, 15000, "#", "say:欢迎使用小樱桃智能语音产品,请直拨分机号,查号请拨0", "say:输入错误", "^(0|10[0-1][0-9]$)", "digits", 2000)
if digits ~= "" and digits ~= nil then
if digits == "0" then
session:transfer("1000 XML default")
else
session:transfer(digits .. " XML default")
end
else
session:speak("再见")
end
我们可以保存上述lua到FreeSWITCH的scripts目录下,命名为welcome.lua,配置完成后就可以在控制台上进行如下测试(依然呼叫1001,接听后进入ivr菜单)
代码语言:javascript复制freeswitch> originate user/1001 &lua(welcome.lua)
电话接听后,我们会听到“欢迎使用小樱桃智能语音产品,请直拨分机号,查号请拨0”这样的欢迎词,可以看到,欢迎词这次我们没有使用录制好的语音文件,而是使用了TTS,上述的TTS使用的是edge-tts。
我们按0#可以实现和IVR XML一样的效果,相应的按1001~1019就可以转到相应的分机路由上去。需要注意的是上述我们讲到的是要按0#,当然我们也可以只按一个0,但是需要等2秒超时,按#是为了告诉程序,我们的按键结束,可以不用等超时,程序继续往下走
下面我们介绍playAndGetDigits
代码语言:javascript复制digits = session:playAndGetDigits (
min_digits, max_digits, max_attempts, timeout, terminators,
prompt_audio_files, input_error_audio_files,
digit_regex, variable_name, digit_timeout,
transfer_on_failure)
- min_digits:最小按键长度
- max_digits:最大按键长度
- max_attempts:当按键不匹配或者没有收到按键时,容忍的次数
- timeout:等待按键的超时时间,单位是毫秒
- terminators:按键结束符
- prompt_audio_files:初始播放的文件,可以是录制好的音频文件,也可以是TTS等。如果播放过程中收到按键,则播放会被打断。如果没有收到按键,会重复播放,直到max_attempts。
- input_error_audio_files:当接收到不匹配digit_regex的按键时播放的音频文件。如果不使用此功能,可以设置一个空字符串,或者可以播放一个静音的stream,比如silence_stream://1000
- digit_regex:验证接收到的按键的正则表达式
- variable_name:可选参数,用于存按键的通道变量
- digit_timeout:可选参数,按键之间的超时参数。
- transfer_on_failure:可选参数,按键失败之后执行的动作,语法是extension-name [dialplan-id [context]],比如1000 XML default
需要注意的一点是在Lua中使用playAndGetDigits和在XML Dialplan中使用play_and_get_digits功能一样,只是参数稍有不同,前者参数digit_regex在variable_name之前,后者反之,读者注意不要弄反了。
对于上面的例子,可能会有读者问,上面的Lua我们可以不可以只按0,同时又不用等2秒超时,答案是肯定的。下面我们简单优化下上面的Lua脚本。
代码语言:javascript复制local tts_engine = "tts_commandline"
local tts_voice = "zh-CN-XiaoxiaoNeural"
session:set_tts_params(tts_engine, tts_voice)
session:setVariable("tts_engine", tts_engine)
session:setVariable("tts_voice", tts_voice)
session:answer()
session:sleep(1000)
local first_digit = session:playAndGetDigits(1, 1, 3, 15000, "#", "say:欢迎使用小樱桃智能语音产品,请直拨分机号,查号请拨0", "say:输入错误,请重新输入", "[0-1]", "first_digit", 2000)
if first_digit ~= "" and first_digit ~= nil then
if first_digit == "0" then
session:transfer("1000 XML default")
else
local remain_digits = session:playAndGetDigits(3, 3, 3, 2000, "#", "silence_stream://1000", "say:输入错误,请重新输入", "^(0[0-1][0-9]$)", "remain_digits", 2000)
session:transfer(first_digit .. remain_digits .. " XML default")
end
else
session:speak("再见")
end
代码语言:javascript复制上述优化过的脚本,我们可以看到,分两步来收集按键,先收第一个按键,因此min_digits和min_digits设置成1即可,这样可以避免按井号和等待按键超时。第一个按键收集之后,可以根据实际再收余下的按键。
上面我们实现了一个很简单常见的IVR场景,学会了简单的流程,读者可以结合实际,写出功能更强大的IVR脚本,好记性不如烂笔头,现在就来动手来写一个吧。
文献参考:
- https://freeswitch.org/confluence/display/FREESWITCH/Lua API Reference
- https://freeswitch.org/confluence/display/FREESWITCH/mod_dptools: play_and_get_digits