ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

FreeSWITCH对接讯飞ASR/TTS:MRCP协议与对话系统集成实践

FreeSWITCH对接讯飞ASR/TTS:MRCP协议与对话系统集成实践 我自己经手过好几套呼叫中心智能语音项目对FreeSWITCH接ASR/TTS这件事算是有比较完整的落地经验。经常有同行问我项目里用的FreeSWITCH到底怎么才能跟讯飞的语音能力打通标题这串东西看着长拆开其实是一条固定的技术链路——FreeSWITCH负责呼叫控制和媒体转发MRCP协议负责让FreeSWITCH跟语音引擎说话讯飞ASR负责把用户语音转成文字讯飞TTS负责把机器人的回复变成语音最后再加一个对话系统来承接业务逻辑。这套东西组合起来就是一个完整的智能外呼/语音机器人/智能IVR项目。这篇文章我就拿我近期的一个实际项目为背景把整套对接过程从方案选型、环境部署、配置实现到问题排查全部过一遍。内容偏向工程实操不会去扯太多理论但每个关键选择我都会说清楚为什么这么干。正好在搭这套系统或者准备接讯飞语音能力但还没理清思路的同学可以直接照这篇文章当参考。1. 项目需求与整体方案设计1.1 这个标题背后到底要解决什么问题“FreeSwitch采用mrcp协议对接科大讯飞asr和tts以及对话系统”这句话看着就是一个技术组合但实际产品需求通常是这样的用户打进来一个电话FreeSWITCH接起来之后播放一段欢迎语然后用户说话系统把语音转成文字再交给对话系统理解用户意图生成应答文本最后再由语音合成播报给用户。整个流程还要支持多轮对话要能随时打断要能识别静音超时。在这个流程里FreeSWITCH的角色不是那个最聪明的部分但是最核心的“水管工”。它要把通话双方的媒体流管理好把音频从一个端口转到另一个端口同时又要把MRCP客户端和RTP媒体流的对接做好。讯飞ASR/TTS的能力再强如果FreeSWITCH这一层没有把媒体流转发对后面全部白搭。所以项目实际要解决三件事一是通话媒体链路怎么建立二是语音识别和合成的请求怎么发出去三是识别结果怎么送到对话系统、对话系统的回复怎么拿回来。MRCP协议解决的是第二件事FreeSWITCH的diplan和API解决的是第一和第三件事。1.2 为什么选择MRCP而不是直接调讯飞SDK我最早接触这个需求的时候第一反应也是能不能直接调讯飞开放平台的SDK毕竟讯飞MSC SDK在移动端和嵌入式项目里用得非常多大多数开发者对它也更熟悉。但放在FreeSWITCH这个场景里直接调SDK是很别扭的原因有三个。第一FreeSWITCH的媒体流是RTP包SDK接收的是PCM音频流中间必须有人把RTP解出来、去抖动、做回声消除、做VAD静音检测这些工作全部自己做一遍成本太高。第二FreeSWITCH本身是事件驱动的并发模型一个呼叫处理涉及很多回调如果在主流程里直接嵌入闭源SDK遇到性能瓶颈或者连接卡死定位问题会非常痛苦。第三MRCP是一个标准协议讯飞、阿里、腾讯甚至自研引擎都能接入换引擎不用改FreeSWITCH的diplan逻辑。MRCP解决的问题就是把“录音转写”这件麻烦事标准化。FreeSWITCH通过mod_unimrcp模块作为MRCP客户端把RTP音频流送到MRCP服务器MRCP服务器内部再跟讯飞语音引擎交互。FreeSWITCH只关心MRCP协议层面的请求和响应不用关心讯飞SDK内部怎么实现。这个对工程维护来说非常重要。1.3 其他可行方案的对比除了MRCP这条主流路线我也见过有人用别的方式把FreeSWITCH和语音识别服务对接起来各有各的适用范围。一种是直接用FreeSWITCH的mod_tts_command模块调外部TTS命令行工具比如espeak或者讯飞离线命令行的工具。这种方式配置简单但是只能做TTS不能做ASR解决不了语音识别的问题而且每次合成都要起一个外部进程效率很低并发一大就直接打满CPU。还有一种是通过ESLEvent Socket Library在FreeSWITCH外部监听呼叫事件收到用户语音后录音存文件再异步调用讯飞的文件转写API。这种方案的好处是完全不用改FreeSWITCH识别逻辑全在外部平台控制缺点是交互时延很大做不到实时打断只适合离线质检这类场景不适合做实时语音对话机器人。相比之下MRCP方案能支持流式识别、支持barge-in打断、支持识别过程中动态控制媒体流这些能力对语音交互项目来说是刚需。所以只要做实时对话型产品MRCP基本是唯一靠谱的选择。这里要特别提醒一句如果只是做几路并发的小规模试验直接调SDK也不算大问题一旦要支撑几十路甚至上百路并发老老实实走MRCP。2. 核心组件部署与准备工作2.1 整套系统到底由哪些组件组成这个标题里的几个技术词对应到实际部署环境里是这些组件组件作用版本建议FreeSWITCH呼叫控制、媒体转发、MRCP客户端1.10.x 或 1.8.xuniMRCP Server真正的MRCP服务器对接语音引擎1.6.0及以上讯飞语音引擎插件让uniMRCP可以调用讯飞ASR/TTS能力讯飞官方提供对话系统接收ASR识别文本返回应答文本自建或第三方平台FreeSWITCH对外提供SIP接口对内部通过mod_unimrcp跟uniMRCP Server走MRCP协议。uniMRCP Server再通过讯飞提供的插件库向讯飞语音服务发请求。对话系统不直接跟讯飞打交道它是通过FreeSWITCH的diplan或者外部ESL拿识别结果再返回回复文本给FreeSWITCH进行TTS播放。这里容易有一个误解有人觉得uniMRCP就是讯飞的东西。其实uniMRCP是开源社区的项目由各大语音引擎厂商或开发者各自写插件才能把特定引擎接进去。讯飞一般会提供适配uniMRCP的插件包也可能要求通过MSC/独立SDK的接口自带开发具体要看商务合作时拿到的交付物。2.2 FreeSWITCH安装与mod_unimrcp编译要点我这次的实验环境是Ubuntu 20.04FreeSWITCH版本用的1.10.7。如果是生产环境我建议优先用系统自带包管理安装官方发布的稳定版本避免自己从源码编译踩一堆依赖坑。但mod_unimrcp比较特殊很多发行版的默认编译开关里没有把它编进安装包得确认一下。用源码编译的话需要保证系统已经安装了libtool、automake、autoconf、libssl-dev、libtool 等基础依赖。FreeSWITCH源码目录里进入src/mod/applications/mod_unimrcp单独编译这个模块再把生成的mod_unimrcp.so拷贝到modules目录下。编译完成后在modules.conf.xml里确认下面两行没有被注释load modulemod_unimrcp/ load modulemod_sofia/如果是Windows环境也有已经编译好的FreeSWITCH发行包可以下载Windows下做功能验证和开发调试完全够用但生产环境我强烈建议放Linux上。Windows版FreeSWITCH自带的模块相对全一些反而省去自己编译的麻烦。加载mod_unimrcp之后用fs_cli执行“show modules”应该能看到unimrcp模块已经加载。如果加载失败绝大多数原因是缺库或者配置文件里的路径不对。查看FreeSWITCH启动日志里面有明确的模块加载错误信息。2.3 uniMRCP Server部署和讯飞插件安装uniMRCP Server是整个MRCP链路里的核心中转站。它的安装方式通常是把官方源码包解压后执行configure和make。安装完成后核心的可执行文件是unimrcpserver配置目录里有unimrcpserver.xml和plugin配置文件。讯飞的插件安装一般会提供两个部分一个是讯飞MSC核心库另一个是适配uniMRCP的插件动态库。插件库里会包含ASR识别引擎和TTS合成引擎的资源适配层。安装完成后插件动态库要放到uniMRCP Server的plugin目录下并确保运行用户对库文件有读取权限。我碰到过的最常见问题就是直接把讯飞SDK的库拷贝到系统目录但忘了在unimrcpserver.xml里声明插件资源映射。结果就是ASR请求发过去uniMRCP Server完全不认识对应的resource。正确做法是确认unimrcpserver.xml中的resource-map配置把MRCP标准资源名映射到实际的插件模块上。另外要确认讯飞SDK所需的动态库路径已经写到LD_LIBRARY_PATH或者把so文件放到系统库目录。讯飞SDK的库经常依赖一堆小动态库缺任何一个插件加载就会静默失败这个排查起来非常恶心只能靠ldd命令一个一个查。3. 配置实现与核心流程串联3.1 配置MRCP Profile文件FreeSWITCH的mod_unimrcp通过conf/mrcp_profiles目录下的XML文件管理MRCP服务器的连接信息。每台MRCP服务器对应一个profilename属性就是diplan里引用时的名字。我通常会在mrcp_profiles目录下建一个xunfei.xml内容类似这样include mrcp_profile namexunfei version2 param nameserver-ip value127.0.0.1/ param nameserver-port value5090/ param nametransport valuetcp/ param namertp-ip value127.0.0.1/ param namertp-port-min value40000/ param namertp-port-max value41000/ param namesip-ip value127.0.0.1/ param namemedia-sync valuetrue/ param nameignore-rc valuefalse/ /mrcp_profile /includeserver-ip和server-port是uniMRCP Server的监听地址。默认情况下unimrcpserver监听5090端口MRCP v1走UDPMRCP v2走TCP或SIP。我推荐使用MRCP v2正式环境基本都是TCP稳定性和调试体验都更好出问题还能抓包看SIP/MRCP消息定位效率完全不一样。rtp-ip和sip-ip这两个参数非常关键它们决定了FreeSWITCH在跟MRCP服务器建立SIP会话时通告的IP地址。如果FreeSWITCH和uniMRCP Server在同一台机器上可以都填127.0.0.1。如果是分布式部署必须填服务器实际网卡上的可路由IP尤其注意不要填成公网IP或内网不通的地址否则媒体流会出现RTP单向通的情况。media-sync参数决定是否做媒体同步。如果后面发现ASR识别到的内容有杂音、丢字、识别准确率低可以试着把media-sync设为true。这个参数的作用是让FreeSWITCH对发送给MRCP服务器的RTP包做缓冲补偿抵消网络抖动带来的影响实测对识别效果提升比较明显。3.2 uniMRCP Server端配置讯飞引擎参数uniMRCP Server侧的配置比FreeSWITCH一侧稍微复杂。核心要改两类内容一是资源映射二是讯飞引擎参数。资源映射把MRCP标准资源speechrecog和speechsynthesizer映射到讯飞插件引擎参数则设置appid、密钥、采样率、识别语言等。在unimrcpserver.xml中关键配置段类似下面mrcp-server profile namexunfei version2 resource-map resource mapspeechrecog nameXunfeiASR/ resource mapspeechsynthesizer nameXunfeiTTS/ /resource-map param namelog-prefix valueXunfei/ /profile /mrcp-server讯飞引擎插件自身的配置在不同版本里叫法不太一样但核心参数基本是这几类app_id、api_key、api_secret三个鉴权信息sample_rate采样率language语言模型。生产环境建议把音频采样率统一设置为8000因为电话语音本身就是8K采样率讯飞模型对此优化最成熟边的16K反而可能因为频段不匹配导致识别率下降。我踩过一次很深的坑就是忘记同步采样率。FreeSWITCH侧的L16协商成了16K但讯飞插件配置的是8K结果uniMRCP Server转出来的音频全是畸形数据识别结果完全不可用。后来统一在FreeSWITCH侧设置8K采样率并且在讯飞插件里也固定8K问题立刻消失。3.3 FreeSWITCH dialplan中的ASR/TTS调用方式配置好profile之后真正在业务里调用就是用dialplan应用。先从一个最简单的例子看起这个分机号码是6000演示了从应答、播报、识别到再次播报的完整流程extension namemrcp_demo condition fielddestination_number expression^6000$ action applicationanswer/ action applicationset datatts_enginexunfei/ action applicationset datatts_voicexiaoyan/ action applicationspeak datatext:您好我是智能客服请问有什么可以帮您/ action applicationplay_and_detect_speech datasilence_stream://1000 xunfei max_time8000/ action applicationlog dataERR 识别结果${detect_speech_result}/ /condition /extension第一行answer是接通电话这没问题。接着set两个变量tts_enginexunfei是让speak应用走xunfei这个MRCP profiletts_voicexiaoyan是设置发音人比如xiaoyan是女声xiaofeng是男声。然后speak应用就会把文本交给讯飞TTS合成并播放。播放完之后play_and_detect_speech这个应用开启识别。它有两个关键参数第一个参数silence_stream://1000是一段静音提示音作用是让FreeSWITCH在开始识别前先播放1秒钟的静音给用户一个短暂的停顿同时把麦克风打开第二个参数xunfei指定MRCP profilemax_time8000表示最大识别时长8秒。识别完成后结果会存在通道变量detect_speech_result里。日志里打出来能看到识别文本实际项目里这一步就是接对话系统的入口。关于speak应用不同FreeSWITCH版本的写法略有差别但核心逻辑是一样的。有的版本支持在speak里直接指定引擎和文本比如action applicationspeak dataxunfei|text:您好请问有什么可以帮您/如果发现语音合成不生效优先检查tts_engine变量是否设置正确以及uniMRCP Server日志里有没有收到synthesize请求。3.4 把对话系统接入完整流程到这里FreeSWITCH和讯飞的ASR/TTS已经通了但离标题里说的“对话系统”还差一步。最简单粗暴的方式是在dialplan里用curl应用把识别结果POST到对话系统接口再拿返回文本播放。比如对话系统接口地址是http://127.0.0.1:9900/api/chat接收一个text字段返回json里有reply字段dialplan可以这样改写extension namemrcp_bot condition fielddestination_number expression^6001$ action applicationanswer/ action applicationset datatts_enginexunfei/ action applicationspeak datatext:您好请问有什么可以帮您/ action applicationplay_and_detect_speech datasilence_stream://1000 xunfei max_time8000/ action applicationcurl datahttp://127.0.0.1:9900/api/chat content_typetext/plain http_methodPOST data${detect_speech_result} result_to_varchat_answer/ action applicationset dataanswer${parse_json(${chat_answer}, reply)}/ action applicationspeak datatext:${answer}/ /condition /extensioncurl应用会把detect_speech_result作为POST请求的body发出去响应内容存到chat_answer变量里再用parse_json函数取出reply字段最后通过speak播报出来。这段配置看起来很简单但生产环境要注意一个很现实的问题curl是同步阻塞的如果对话系统接口响应慢用户会在电话里沉默很久。所以对话系统接口的响应时间必须尽量控制在500毫秒以内而且FreeSWITCH侧要加超时控制。更工程化的做法是把对外集成放到FS外部用ESL监听呼叫事件。FreeSWITCH这边只负责识别和放音识别完成后触发一个自定义事件交给外部服务外部服务异步调用对话系统然后通过ESL命令控制FreeSWITCH播放TTS结果。这样做的好处是业务逻辑完全跟呼叫控制解耦。一旦对话系统升级或者接口变化不需要动FreeSWITCH的dialplan只要改外部服务。4. 常见问题排查与工程化优化4.1 ASR识别结果为空或者忽然断掉这个是我被问得最多的一个问题。用户明明说话但程序走到play_and_detect_speech这里之后一直等到max_time耗尽detect_speech_result里还是空的。细查之后发现主要分两类原因。第一类是媒体流没有到达uniMRCP Server。FreeSWITCH的RTP流没有正常转发或者uniMRCP Server收不到音频包。这种问题可以用tcpdump在uniMRCP Server所在网卡抓包看有没有往8000/40000端口发的RTP包。如果只有信令没有媒体再回查rtp-ip参数是不是填了一个不可达的地址。第二类是语音引擎识别到了内容但FreeSWITCH侧没取到结果。这种时候要去翻uniMRCP Server日志看有没有把recognition event返回给客户端。如果unimrcp那边有识别结果而FreeSWITCH的日志里没有detect_speech_result很可能是MRCP协议交互中途出了问题可以在FreeSWITCH里打开unimrcp模块的调试日志看具体的请求和响应。还有一个容易被忽略的点是barge-in错误。用户在TTS播报还没结束的时候就开始说话按正常逻辑应该打断播报并开始识别但如果打断事件处理不对识别会被直接终止且不返回结果。FreeSWITCH里可以用play_and_detect_speech的no_bargein参数控制打断模式或者提前在TTS播放前加一段静音先处理掉用户习惯性的“抢话”行为。4.2 TTS播报声音卡顿或不完整TTS播报卡顿很多时候不是讯飞引擎的问题而是FreeSWITCH侧RTP发送节奏不对。MRCP v2的TTS合成流式返回但FreeSWITCH播放音频的时候如果RTP时间戳和实际发送间隔对不上就会出现声音忽快忽慢或者偶尔断掉。这种问题我习惯先从uniMRCP Server日志看合成返回的音频参数。如果合成音频是16kHz而通道里协商的是8kHz先做音频重采样。如果采样率一致仍有问题再排查FreeSWITCH的媒体bug开关有时候要设置enable-rtp-bugs参数。我遇到过一种很隐蔽的情况TTS播报偶尔会漏掉最后几个字听起来像是“您好请问有什么可以帮”然后就停了。查了半天发现是FreeSWITCH侧在MRCP的speak返回完成事件之后立刻拆掉了RTP会话导致最后一小段RTP包还在缓冲区里没发出去。解决办法是在播报结束后加一个极短的静音播放给RTP缓冲区留出清空时间。4.3 早媒体场景下的识别问题如果你做的是外呼机器人大概率会遇到早媒体识别场景就是FreeSWITCH还没answer之前就需要识别用户语音来判断是否有人接听。这个场景下电信运营商会把被叫的彩铃或者提示音混在媒体流里直接开启ASR往往会识别到一堆乱七八糟的内容。FreeSWITCH处理早媒体的思路是用ring_ready配合p-early-media-support变量控制把早期媒体交给MRCP引擎做实时检测。具体配置是在dialplan里设置action applicationset datap-early-media-supporttrue/ action applicationring_ready/ action applicationdetect_speech dataxunfei start/但这里的关键坑在于早媒体识别一旦开启用户还没摘机的静默音频也会被当成真实语音送过去有些噪声还会误触发“有人说话”事件。解决思路是先放一段预识别把纯静音的音频能量基线测出来再结合VAD阈值过滤只有能量超过阈值的音频才真正交给ASR。这部分逻辑讯飞引擎自身可以配置但工程上我更建议在FreeSWITCH侧做一层前置判决。4.4 并发性能优化与参数调优语音机器人的并发能力最终取决于ASR和TTS引擎的处理能力而FreeSWITCH和uniMRCP Server在中间决定能转发多少媒体流。我实测下来单台4核8G的服务器跑FreeSWITCH加uniMRCP Server做8K采样率的ASR和TTS大概能支撑30到50路并发但前提是dialplan里没有明显阻塞操作而且讯飞接口响应要快。要提升并发有几个关键参数值得调。第一是uniMRCP Server的线程池和会话并发数很多版本默认并发数很小在unimrcpserver.xml里找到session相关配置调大。第二是FreeSWITCH的media线程配置如果每个呼叫都启用了太多媒体处理功能CPU很快就耗尽尽量减少不必要的录音和转码。第三是TTS缓存对于固定话术可以把合成好的音频文件缓存下来下次直接播放文件不经过MRCP引擎能大幅降低引擎压力。我有一套验证过的处理方式把高频话术的TTS结果在项目初始化时预生成好存成一个audio目录dialplan里先用文件判断是否存在如果存在直接play如果不存在才走speak。这个优化对降低TTS引擎负载效果非常明显尤其是在外呼场景里开场白和结束语几乎都是重复的。对话系统那边的响应速度也直接决定并发上限如果对话接口平均响应时间从300毫秒涨到1秒每路呼叫占用的FreeSWITCH线程就会明显拉长通路上更容易堆积呼叫。所以对话系统单接口的TPS必须提前压测别等上线了才发现接口扛不住。4.5 一套值得养成的调试习惯最后聊几个我长期踩坑总结出来的调试习惯适合所有正在接MRCP这套方案的人参考。第一永远先查日志而且要看全。FreeSWITCH日志、uniMRCP Server日志、讯飞SDK日志各看各的不要混在一起最好把时间戳对齐一条调用链能串起来看排查效率会高很多。UNI MRCP Server日志里如果能看到synthesize和recognize的请求响应说明MRCP协议层面是通的问题大概率在音频质量或引擎配置。第二用抓包工具确认关键信息。MRCP基于SIP和RTP用Wireshark抓包能直接看到SDP里的媒体参数。很多诡异的问题比如识别不出来、播报卡顿抓包一看就知道是SDP协商成了错误的编码格式比盲猜日志快得多。第三不要小看网络延迟。FreeSWITCH和uniMRCP Server跨机房部署的场景下RTP单向延迟超过几十毫秒就会明显影响识别实时性。如果对话系统还要跨公网访问延迟更高。生产环境尽量把FreeSWITCH、uniMRCP Server、对话系统放同一个内网而且最好同一个可用区这是成本最低但收益最高的性能优化。我之前有一次线上问题用户说前几个字总是识别不到排查到最后发现是FreeSWITCH和uniMRCP Server分别在两个云可用区RTP包绕了外网走识别引擎收前100毫秒音频的时候数据还没全部到达。把uniMRCP Server迁到跟FreeSWITCH同一个可用区之后问题再没出现。4.6 关于对话系统状态衔接的一些补充如果你做的项目需要多轮对话比如“请问您要办理什么业务”“好的请提供您的手机号”“请确认您需要查询的是本机号码吗”这里还藏着一个对话系统状态管理的问题。ASR每次识别出来的是单轮文本但对话系统需要知道当前处在哪个状态才能正确解析意图。MRCP协议本身不管状态FreeSWITCH的dialplan变量可以用来存全局状态但更推荐把状态放到对话系统侧维护。每次ASR识别完FreeSWITCH把识别文本和当前session_id发给对话系统对话系统根据session_id拉取历史状态生成应答文本后返回。这个session_id可以是FreeSWITCH的通道变量uuid也可以自己生成一个业务流水号。我习惯直接用${uuid}变量因为它天然唯一还能关联到通话记录。多轮对话场景还有一个容易踩的坑用户不按套路回答。比如问到“请提供手机号”用户来一句“我不记得了”对话系统如果没有兜底话术就会卡住。工程上要在对话系统侧设计好“未识别意图”和“澄清确认”两条分支同时FreeSWITCH侧要控制最大误识别次数超过三次转人工避免用户体验崩掉。把ASR、TTS、对话系统通过MRCP这条链路串起来其实只是智能语音项目的第一步。真正的复杂度在业务状态管理、异常兜底和并发性能上这些都要靠实战去填坑。上面这些配置和调试方法都是我实际项目里验证过的可以直接拿去做参考。如果你是第一次搭这套环境建议先在一台机器上把FreeSWITCH、uniMRCP Server全部装好用6000分机把最简单的识别放音流程跑通再逐步加上对话系统和多轮逻辑整个排查过程会轻松很多。
返回列表