在不涉及非必要人员的前提下与来电者交互,是一种古老的电信行业特长。
自助导航和交互式语音应答是两个最重要和最最受欢迎的服务,几乎所有组织的通信平台上都能够看到它们的身影。
无论是简单的调度员,还是复杂的IVR,FreeSWITCH都能胜任。调度员业务诸如自助导航路由:“销售部请按1,售后支持请按2”;复杂的IVR业务比如搜集呼叫者身份证明,然后连接数据库以查询其帐户余额。这些都不在话下。
但是,我们播报给呼叫者的消息要如何处理呢?我们需要录制所有客户可能听到的短语吗?不!FreeSWITCH有一种"phrase"结构,它提供一种灵活的机制,可以根据需要,利用短小的声音文件,组合出任意短语,而且它是实时、动态组合的。然后呢?是的,还有文语转换(TTS)!
短语宏与提示音
短语宏是FreeSWITCH重用、连接和组合声音文件的智能方法,用于合成电话中播放的提示音。
提示音就是你呼叫IVR或数字助手时听到的声音。最常见,也是最烦人的提示音估计是:所有话务员正忙,请稍候。
提示音的前世今生
过去,提示音有两种录制方式:一种是是非常昂贵的,由专业播音员在专业工作室使用高端音频设备录制,并且由声音工程师进行切割抛光等后期制作。另一种是免费的,像喝啤酒一样,自己动手,非常类似于公司秘书录制的答录消息,不专业,有背景噪音,声音抖动......
这两个极端之间什么都没有。另外,在录制和重新录制新提示音,朗读文本间耗费了许多工作。这样的努力(和成本)导致应用程序无法经常为用户更新文本。
后来出现了一些TTS应用,它们合成的声音听起来有点机械,很有趣,就像60年代B级电影中的机器人,但从某种程度上来说,它们完全够用。人们很快就适应了“机器人”的声音。唉,可是它依然是很昂贵的,这类服务通常按并发通道数收取授权费,集成实现也需要开销。
FreeSWITCH的声音文件
FreeSWITCH最受欢迎的特性之一,是它能够从一些有限的(不是很大的)声音文件开始,与呼叫者建立起几乎任何类型的交互语音。这些文件存储在/usr/local/freeswitch/sounds目录下(音乐音频文件也存放在这里的"music"子目录中)。
这些声音文件由顶级声音艺术家在超级音频工作室中进行专业录制,我们甚至有一套完整的新设备,由电信女神Allison Smith完美诠释,它发布于2017年的ClueCon年会(ClueCon是我们的开发者年会,主题涉及VoIP 、WebRTC、FreeSWITCH、物联网和所有实时通信话题。每年8月在芝加哥举行)。安装FreeSWITCH后,所有这些文件我们都免费提供。
FreeSwitch发行版中的声音文件包含单个单词的发音、数字、关联短语等:
声音文件的位置,采样率和回退
在FreeSWITCH的配置中(包括拨号方案、IVR、短语、脚本),可以用绝对路径(以斜杠“/”开头)引用声音 文件,也可以用相对路径。使用相对路径时,文件解析为$${sound_prefix}/relative_path,其中relative_path以声音文件的采样率(频率以赫兹为单位)进行分组(因此freeswitch不必重新采样)。
你可以在/usr/local/freeswitch/conf/vars.xml中找到全局变量$${sound_prefix}的定义值,或者通过FreeSWITCH控制台执行以下命令查询:
eval $${sound_prefix}
举例说明:你呼叫演示配置中的5000。这时电话被引导到IVR自动应答。这个IVR定义在/usr/local/freeswitch/conf/ivr_menus/demo_ivr.xml文件中。它以"demo_ivr_main_menu"短语宏开始(这个宏定义在/usr/local/freeswitch/conf/lang/en/demo/demo-ivr.xml)。这个宏的第一项是:
<action function="play-file" data="ivr/ivr-welcome_to_freeswitch.wav"/>
如果你的呼叫使用PCMU编码(比如说G711),因为这种编码的采样率是8khz,所以,相对路径"ivr/ivr-welcome_to_freeswitch.wav"将会被解析为” /usr/local/freeswitch/sounds/en/us/callie/ivr/8000/ivr-welcome_to_freeswitch.wav”,IVR将读取并播放这个文件。如果这个文件不存在,FreeSWITCH将首先使用48khz采样率的同名文件(/usr/local/freeswitch/sounds/en/us/callie/ivr/48000/ivr- welcome_to_freeswitch.wav),因为FreeSWITCH音频混音的格式是48khz的。如果还找不到,再尝试32khz,再然后是16khz。如果还是没找到,FreeSWITCH会查找/usr/local/freeswitch/sounds/en/us/callie/ivr/ivr- welcome_to_freeswitch.wav文件(在路径中不插入采样率),并通过自动检测采样率播放文件。如果所有地方都找不到,FreeSWITCH将输出一条错误日志并继续播放下一个音频文件。
短语宏:连接声音文件
短语是一种FreeSWITCH配置的XML结构,它允许你基于简单的声音文件构造复杂的提示音。声音文件会一个接一个提取,中间插入设定的静音时间。
短语宏定义于/usr/local/freeswitch/conf/lang/en/en.xml("en"表示英语,可以用其它国际语言代码替代,比如"fr"表示法语)。
让我们看看演示配置中最流行短语宏配置的第一行,它定义在/usr/local/freeswitch/conf/lang/en/demo/demo-ivr.xml文件中:
<macro name="demo_ivr_main_menu" pause="100">
<input pattern="(.*)">
<match>
<action function="play-file" data="ivr/ivr- welcome_to_freeswitch.wav"/>
<action function="play-file" data="ivr/ivr- this_ivr_will_let_you_test_features.wav"/>
<action function="play-file" data="ivr/ivr-
you_may_exit_by_hanging_up.wav"/>
...
...
</match>
</input>
</macro>
macro name (pause)
macro name指定宏的名字,是你在拨号方案或脚本中调用短语时引用的内容。比如:"phrase:demo_ivr_main_menu"。参数"pause"是可选的,单位是毫秒,指定每个声音文件播放结束后休眠的时间。如果你省略了"pause"参数,声音文件就会被一个接一个无缝地播放。你也可以在"play-file"之后插入一个"sleep",以达到休眠的目的。
input pattern
"input pattern"里的正则表达式将检查传递给短语宏的参数。如果表达式匹配,那么将执行"match"片段定义的action;如果不匹配,则执行"nomatch"片段所定义的action。这里的input pattern表达式为".*",它将匹配任意输入,甚至是没有输入的场景(事实上,大多时候你不会向短语传递任何输入参数)。
match - nomatch
请参考前一节:input pattern。
action
这个XML标记定义短语宏要执行的动作。通常是"play- file"或"sleep"。这两个操作是连接音频文件的主要工作。另一个有用的action叫"say",它以特定的语言(英语、汉语等)规则,正确播报数字、日期、数量、货币、序号、拼写等信息。通过"execute" action,你可以调用任意一个FreeSWITCH API。
短语宏:调用,模式与寄存器
XML和正则表达式为短语宏机制提供了许多强大的功能。之前我们见过的"input pattern"就是非常有用,它不仅能够决定执行的指令内容("match"和"nomatch"标记),还能够构建动态短语。别着急,继续往下读。
短语宏调用
调用宏,可以传参,也可以不传参。因此。它的语法格式是这样的:
phrase:macroname[:one or more arguments]
如果有参数,宏名字(第二个冒号)后面的所有内容(包括空格),将作为一个完整参数传递给宏。
输入模式和寄存器
如果模式匹配成功,短语宏可以根据模式中的圆括符从寄存器中读取输入的部分内容。这和拨号方案中的模式“表达式”完全相同:我们假设input pattern是"^\d(\d{3})(\d)(.*)$",并且输入参数是"1234567ciao",那么在宏里面,会有这么几个变量,值为"234"的$1;值为"5" 的$2;还有值为"67ciao"的$3。
在宏里面,可以利用寄存器机制读取变量内窜并反馈给呼叫者。让我们看一个实例,在宏里面读取用户语音信箱里的消息数量(新的消息或保存的消息):
<macro name="voicemail_message_count">
<input pattern="^(1):(.*)$" break_on_match="true">
<match>
<action function="play-file" data="voicemail/vm-you_have.wav"/>
<action function="say" data="$1" method="pronounced" type="items"/>
<action function="play-file" data="voicemail/vm-$2.wav"/>
<action function="play-file" data="voicemail/vm-message.wav"/>
</match>
</input>
<input pattern="^(\d+):(.*)$">
<match>
<action function="play-file" data="voicemail/vm-you_have.wav"/>
<action function="say" data="$1" method="pronounced" type="items"/>
<action function="play-file" data="voicemail/vm-$2.wav"/>
<action function="play-file" data="voicemail/vm-messages.wav"/>
</match>
</input>
</macro>
在我们的案例中,只有一条新的留言消息,所以调用宏时是这样的:"phrase: voicemail_message_count:1:new."。第一条input patterns会匹配成功,因为参数中的第一个字符是"1"。此外,模式中的第一个字符是包含在一对圆括符中的,因此,输入的第一个字符(字符1)将被提取到$1。输入参数的其余内容,冒号之后又出现一对圆括符(直到模式结束),因此,冒号后面的内容将被存储到$2。在本例中,$2的值将是字符串"new."。接下来,宏会执行"match"标记指定的action,然后退出,不匹配下一条模式(break_on_match="true")。执行action时用到寄存器存储的值:调用"say"时,$1作为它的参数;调用"play-file"时,$2作为文件名的组成部分传递。另请注意,在这种情况下,模式与一条消息(新消息或已保存消息)匹配时,短语宏将正确播放voicemail/vm-message.wav音频文件(单数格式)。
让我们看一下,当有三条保存的语音留言时,调用宏会发生什么:"phrase: voicemail_message_count:3:saved."。这时第一条input pattern匹配不成功(参数的第一个字符不是"1")。接下来匹配第二条input pattern "^(\d+):(.*)$",这时匹配成功。冒号前的数字部分将被提取到$1寄存器,而$2寄存器的值变成字符串"saved."。这时第二条input pattern的"match"标记设置的指令将会被执行,请注意播放的voicemail/vm-messages.wav文件是复数的读音。
XML IVR
IVR就是我们都很熟悉并喜欢(或许是讨厌)的交互式主意应答系统:“了解特价信息请按2;转人工服务请输入48位会员卡号,以井号键结束”。FreeSWITCH的IVR引擎可以让你在不需要编程的前提下构建语音菜单驱动的应用程序,所以,即便你是市场营销顾问,经过短暂的简单培训之后,你也能够自己动手。
FreeSWITCH的XML IVR基本上是一个复杂的参数化框架,构建一个完整的IVR应用程序需要很多参数,可以配置任意数量的语音菜单项,而执行IVR可以从一个单一的拨号方案extension开始。
每个主意菜单项都会响应用户输入的DTMF按键消息,通常是执行一个拨号方案APP(bridge、transfer、playback等等)。
XML IVR支持很多特性,包括:设置超时,多位输入,TTS集成,设置错误尝试次数,支持子菜单,返回主菜单,菜单重复。
演示配置中的IVR
首先,让我们看看演示配置中的IVR---- extension 500。它定义在/usr/local/freeswitch/conf/dialplan/default.xml文件中,内容如下:
<extension name="ivr_demo">
<condition field="destination_number" expression="^5000$">
<action application="answer"/>
<action application="sleep" data="2000"/>
<action application="ivr" data="demo_ivr"/>
</condition>
</extension>
这段代码是FreeSWITCH配置中最常用的片段之一,因为新安装的环境中,很自然地会用它做测试。相关的一行是<action application="ivr" data="demo_ivr"/>,它将执行拨号方案的"ivr" app,执行参数是"demo_ivr"。
拨号方案的"ivr" app会在FreeSWITCH的配置中查找一个"name"字段值为"demo_ivr"的语音菜单。在本 例中,指向缺省语音菜单配置目录下的一个XML文件:/usr/local/freeswitch/conf/ivr_menus/demo_ivr.xml。文件名可以是完全不同的任意名字,重要的是菜单配置中"name"字段的值。
语音菜单"demo_ivr"的编写目的是让我们体验交互系统构建块的多种特性:发起并桥接呼叫;回音测试;播放保持音乐;连接一部注册上的内部话机;跳转子菜单;返回主菜单;转接拨号方案的其它extension。
<include>
greet-long="phrase:demo_ivr_main_menu"
<menu name="demo_ivr"
greet-short="phrase:demo_ivr_main_menu_short"
invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav"
exit-sound="voicemail/vm-goodbye.wav" confirm-macro=""
confirm-key=""
tts-engine="flite" tts-voice="rms" confirm-attempts="3" timeout="10000"
inter-digit-timeout="2000" max-failures="3"
max-timeouts="3" digit-len="4">
<entry action="menu-exec-app" digits="1" param="bridge sofia/$${domain}/888@conference.freeswitch.org"/>
<entry action="menu-exec-app" digits="2" param="transfer 9196 XML default"/> <!-- FS echo -->
<entry action="menu-exec-app" digits="3" param="transfer 9664 XML default"/> <!-- MOH -->
<entry action="menu-exec-app" digits="4" param="transfer 9191 XML default"/> <!-- ClueCon -->
<entry action="menu-exec-app" digits="5" param="transfer 1234*256 enum"/> <!-- Screaming monkeys -->
<entry action="menu-sub" digits="6" param="demo_ivr_submenu"/>
<entry action="menu-exec-app" digits="/^(10[01][0-9])$/" param="transfer $1 XML features"/>
<entry action="menu-top" digits="9"/> <!-- Repeat this menu -
->
</menu>
<!-- ----------------------------------------------------------- -->
<menu name="demo_ivr_submenu"
greet-long="phrase:demo_ivr_sub_menu"
greet-short="phrase:demo_ivr_sub_menu_short"
invalid-sound="ivr/ivr-that_was_an_invalid_entry.wav" exit-sound="voicemail/vm-goodbye.wav"
timeout="15000" max-failures="3" max-timeouts="3">
<entry action="menu-top" digits="*"/>
</menu>
</include>
一般的工作可能在你第一次阅读时就已经很清楚了。还有,请测试一下,你会发现随着时间的推移,它会变得非常熟悉。在下一节中,我们将研究决定语音XML“菜单”行为的每个参数字段的具体用法,以及如何添加“条目”(例如,当用户发送一些DTMF时,语音菜单将执行的响应操作)。
XML语音菜单字段
你可以把XML IVR中的“菜单”看成一个通用框架,它的不同元素根据菜单定义中的参数改变行为。这个菜单框定义了在第一次迭代时要告诉呼叫者的内容——通常包括一个初始的问候语,如“感谢致电Acme”; 然后是有关IVR用法的说明:“会议请按1,回声测试请按2”,等等。另一个字段定义了后续处理中将播放给呼叫者的内容(比如,你选择按键9重听菜单,这时你不会听到感谢语,而是直接读取说明书)。还有些字段定义了呼叫者挂机之前,或者输入错误(比如说,只允许输入1-3,5-7,*和#时却输入4)时需要读取的内容。
所有定义需要读取内容的字段的取值,可以是一个"phrase"结构,或缺省声音目录中的一个音频文件,或用绝对绝对路径指定的一个音频文件,或是一个"say" TTS结构(换句话说,定义TTS引擎)。
name
"name"字段值就是拨号方案中"ivr" app引用菜单时传递的参数值。它与文件名无关。一个XML文件可以包含多个 菜单定义,它们可以通过"name"字段查找(只要文件存放在ivr-menus目录中就可以)。
greet-long
这是传递给呼叫者的第一条信息内容,你可以包含你的公司名称和对呼叫者的称谓,再加上IVR的用法说明,如“销售部请按1,售后支持请按2”,等等。
greet-short
这里定义返回主菜单时,或者呼叫者希望重听菜单时,需要读取并传递给用户的信息。通常这里的指示很简洁,没有冗长的欢迎辞。
invalid-sound
当呼叫者输入的DTMF与菜单“条目”不符时读取的内容。一般,这时候你需要向呼叫者播报“您的输入有误,请核实”。
exit-sound
退出菜单,用户挂机前播报的内容(因为输入错误超限,超时等原因退出菜单)。
timeout
在greet-long或greet-short播报之后,等待用为输入DTMF的时间,单位是毫秒。如果超时触发,会重复播报欢迎辞,最多尝试max-timeout指定的次数。
inter-digit-timeout
多位输入时,两位DTTMF间的最长等待时间,在输入位数没达到最长位数并且用户没有输入结束标识时生效。如果超时,已经输入的内容视为完整实体,开始匹配菜单条目。
max-failures
菜单转到exit-sound并挂机之前,错误输入的次数限制,每次输入错误,如果没有超限,将播报invalid-sound,并重新播报欢迎辞。
max-timeouts
菜单转到exit-sound并挂机之前,超时重试的次数限定,每次超时,如果没有超限,将播报invalid-sound,并重新播报欢迎辞。
digit-len
单次输入所允许的最大输入位数。例如,你可能希望把它设置为4,这样对应演示配置实例中,"Local_Extension"对应的分机号长度,这样可以接受1001到1019这些号码的输入。允许用户通过“确认键”(通常是#)或者等待超时("inter-digit-timeout.")输入较短的号码序列(不满四位的号码)。
tts-engine
FreeSWITCH配置文件中注册的TTS引擎名称,通常测试用"flite",生产环境用"cepstral"。
tts-voice
这是传递给TTS引擎的"voice"参数值。TTS引擎能够提供不同的声音转换,比如说男声,女声或方言。令人好奇的是,flite TTS引擎提供的"rms"声音是从GNU项目创始人Richard Marshall Stallman的声音采样出来的。
confirm-key
用户结束序列输入时的按键。缺省是#。
XML 语音菜单的entriy 和 action
在"menu"的开始与结束XML标签之间,你需要丰富架构的内容,换句话说:当用户输入DTMF时会发生什么,这才是IVR的主菜。
这就是菜单项的描述,在XML格式中,它们以"entry"为标记,强制携带"action"和"digits"属性,还有一个可选的属性叫"param":
<entry action="menu-play-sound" digits="7" param="say:This is the seventh option"/>
我们将在后面的段落中研究"action"的可能值。"digits"是用户发过来触发菜单执行action的完整DTMF序列(回顾一下之前的digit-len 和 confirm-key);显然,"param"就是传递给action的参数描述。
下面的截图中,我们可以看到拨打演示配置中的"5000",然后按7(我们已经前前面的示例条目添加到IVR中)时输出的调试信息:
menu-exec-app
这是IVR菜单的主力。它允许执行一个FreeSWITCH拨号方案的APP,还可以向APP传递参数。参见第6章,XML 拨号方案中的action"内容。
menu-play-sound
播放一个声音文件(以绝对路径或相对路径定位);或者播放一个"phrase:"结构;或者一个TTS的"say:"结构。
menu-sub
执行(跳转到)另一个XML语音菜单,通过菜单名标识。
menu-back
从一个子菜单跳转回上一层XML语音菜单。
menu-top
从子菜单直接跳转回顶层入口的主菜单。
menu-exit
你可能不相信,但事实在它就是存在,望文生义,它退出菜单!
嵌套XML IVR
所谓嵌套菜单是指一个XML语音菜单条目把话务转到另一个语音菜单,新的菜单有它自己的条目定义,并且有可能"digits"属性和之前的菜单项是相同的。嵌套菜单有两种选择。
其一,可以使用"menu-sub" 这个action,它免费提供"menu- back"补充操作。这对于只连接几个语音菜单、不涉及复杂的菜单间逻辑的简单应用程序非常方便。
另一种场景,如果你不能预测未来的发展方向,可以用"transfer" APP作为menu-exec-app的参数。这样,你将会被转移到拨号方案的另一个extension,你可以编辑拨号方案,让另一个调用"ivr"的语音菜单在那里守候。这种技术在复杂的应用程序中更容易使用,语音菜单之间可以有许多逻辑路径,同时,你可能希望添加或修改菜单。
总结
这一章,我们研究了FreeSWITCH中的两种用户交互方式:
- "phrase:" 结构,它让你可以把预先录制好的声音片段连接起来,实时动态地构建有意义的句子
- 拨号方案的"ivr" APP和XML "菜单"结构,它让你可以在无编程的前提下方便地构建交互式语音应答系统
下一章,我们将研究Lua脚本,把系统与来电间的互动性带到一个新的,不可思议的高度。