
简介这份开源资源面向FreeSWITCH开发者和语音识别集成场景提供一套用C编写的ASR对接模块解决在FreeSWITCH中接入ASR并将识别结果通过ESL输出的需求适合不希望购买商业模块、仅作技术研究的开发者。压缩包共10个文件以so动态库文件为主包含编译好的ASR主模块、适配不同FreeSWITCH版本的模块及配套运行库另有C源码、Visual Studio工程文件、说明文档和配置文件整体仅714KB体积小巧便于下载和快速尝试。目前已有920人浏览学习。使用者既能直接获取编译产物并部署到FreeSWITCH的mod目录也可参照源码和工程理解模块编写思路配置文件中还给出了基础参数示例能帮助完成x64环境下的安装调试并区分FreeSWITCH 1.2与常规版本的加载差异对研究语音识别集成与ESL输出机制有实际参考价值适合作为二次开发的学习起点。 去年我开始在语音网关侧接 ASR自动语音识别能力踩了一路 FreeSWITCH 的坑之后才慢慢把这个FreeSWITCH-ASR 应用程式拼出了完整形态。这个项目本身做的事并不复杂让 FreeSWITCH 在处理电话呼叫时把实时语音流送给识别引擎再把识别结果拿回来驱动后续的呼叫流程。但它涉及到媒体协商、模块编译、并发控制、引擎选型一堆环节并不是装个模块就能跑通的即插即用组件。这篇内容我会用实际项目的角度来拆面向三类读者一类是刚准备在 FreeSWITCH 上做语音识别验证的开发者一类是把 ASR 能力往生产环境推的架构师还有一类纯粹是好奇通信服务器和语音 AI 怎么结合的爱好者。我会从怎么搭 FreeSWITCH 1.10 的编译环境讲起逐步深入到媒体流如何对接 ASR 服务、识别结果如何回到呼叫流程再给出一份国内外 ASR 引擎的对比思路最后是我在真机上排查过的一批典型问题。1. FreeSWITCH 里做 ASR到底在解决什么问题先想清楚一个前提电话线上的音频和普通麦克风采集的音频不太一样。走 SIP/PSTN 进来的语音编码格式通常是 G.711、G.729 或者 opus码率、采样率、帧长都是固定的。这些音频不但要能被识别引擎听懂还要保证实时性不能等整通电话录完再识别否则就失去了对话型应用的意义。所以只要你想在呼叫过程中做边说边识别就绕不开 FreeSWITCH 这个媒体服务器中央节点的存在。1.1 FreeSWITCH 的职责边界FreeSWITCH 在语音链路里扮演的角色相当于一个可编程电话交换机。它负责 SIP 注册、呼叫路由、媒体转发、编解码转换、录音、放音还能执行 Dialplan拨号计划里的逻辑。平时做 IVR交互式语音应答是播放一段提示音然后收 DTMF 按键而接入 ASR 之后按下1或2的按键输入变成了自然语言的语音输入。这个变化带来的工程问题很直接FreeSWITCH 默认只擅长处理媒体流不具备把语音转写成文字的能力。你需要决定连接哪家识别引擎、以什么协议把语音送出去、怎么把识别中间结果和最终结果送回 FreeSWITCH。这个衔接过程需要在 FreeSWITCH 的模块层、事件层和应用脚本层做打通项目里最常见的主导方案是扩展原生模块或者是通过 FreeSWITCH 提供的 socket 接口与外部程序交互。1.2 电话场景对 ASR 的特殊要求ASR 引擎本身的识别精度当然重要但在电话场景里工程指标往往比纸面精度更先卡脖子。延迟敏感度极高用户每说一句话系统需要在几百毫秒内给出反馈如果转写结果回传超过两三秒整个交互就废了。音频中断和噪音电话信道有回声、电路噪声、环境音跟直接拿高保真麦克风录的音完全不是一回事识别引擎需要做对应的音频增强或容忍处理。双讲Double-talk用户跟机器人同时说话时媒体流里会有两个声源ASR 必须能处理这种边听边说的状态否则识别结果会串得没法看。结果驱动的业务逻辑ASR 结果不是展示出来一篇文章而是要映射成业务动作比如订机票、查余额、办挂失所以槽位识别意图理解往往比单纯语音转文字更重要。这些要求在 FreeSWITCH 里落地的难度远大于在手机 App 里调用一个语音识别 SDK。手机端厂商已经把麦克风权限、音频格式、网络回调全部封装好了而在 FreeSWITCH 这一侧从拿到音频帧到吐回识别文本中间每一层都要自己控制。2. 搭一个能跑的 FreeSWITCH 1.10 开发环境依赖与编译FreeSWITCH 的稳定版本中1.10 在 Ubuntu 下的表现比较成熟。很多教程会直接让你apt install freeswitch但真正做 ASR 定制的时候官方仓库预编译包不一定带全你需要的模块源码尤其是要自己添加识别插件时从源码编译几乎是必经之路。2.1 Ubuntu 下的依赖准备我是在 Ubuntu 20.04/22.04 上做的编译验证。官方文档列了一堆依赖但实际编译时最容易缺的是下面这几类基础编译工具链build-essential、autoconf、automake、libtool、pkg-config音频相关库libspeex-dev、libopus-dev、libsndfile1-dev、libpulse-dev数据库与协议库libpq-dev、libldap2-dev、libedit-dev、libssl-dev其他偶发依赖liblua5.2-dev、libcurl4-openssl-dev、libsofia-sip-ua-dev建议按 FreeSWITCH 官方文档里 1.10 的 Debian/Ubuntu 依赖清单逐个装完。如果你是在较新的 Ubuntu 版本比如 22.04上装旧版依赖个别包名会变化遇到Unable to locate package时先apt update再确认包名。2.2 为什么编译时要加 --disable-dependency-trackingFreeSWITCH 源码包编译时最讨厌的就是自动依赖追踪。它会在生成 Makefile 的阶段去扫描所有子模块、自动生成依赖头文件这个机制在处理大型模块树时经常产生意外的 hook 失败比如某个头文件生成顺序错乱、某个子目录的依赖没有同步更新。对于只做 ASR 相关定制的场景我一般会走以下配置cd /usr/src/freeswitch-1.10 ./bootstrap.sh -j ./configure --disable-dependency-tracking --enable-static make -j8 make install--disable-dependency-tracking的主要作用是让编译系统不做深层依赖扫描减少 Makefile 生成的复杂度和构建失败概率。代价是修改头文件之后部分模块可能不会自动重新编译需要手动make clean后重来。对于集中调一个模块的场景这个代价可以接受换来的是更稳定的编译过程和更快的首次构建速度。注意如果在make阶段报出某个模块缺头文件先别急着加全局依赖先确认这个模块是否需要参与编译。ASR 项目通常只需要核心模块加你自己写的模块不需要把所有模块全编出来编出太多不需要的东西只会拖慢后续每次 make 的速度。2.3 一个容易踩的坑mod_av 和 libavcodec 的版本冲突FreeSWITCH 的 mod_av 依赖 FFmpeg 库。Ubuntu 自带的多媒体库版本比较新时会和 FreeSWITCH 1.10 里代码对应的 API 版本不一致导致mod_av编译不过。ASR 项目虽然不一定直接用 mod_av但在执行make all时很多发行版配置默认会把它带上。处理方式就看你的使用场景如果确认用不到音视频转码和录音文件处理可以在 modules.conf 里注释掉applications/mod_av避免这一层冲突。如果一定需要录音功能建议单独安装指定版本的 FFmpeg 开发库或者用 FreeSWITCH 自带的 mod_sndfile 来做简单 WAV 录音把对 mod_av 的需求降到最低。3. ASR 接入设计的核心媒体流怎么给到识别引擎FreeSWITCH 和 ASR 引擎之间最核心的是媒体流对接。不同接法的实现复杂度和实时性差别很大很多人第一次做时在这里纠结很久。3.1 三种常见接入方式接入方式优点缺点适用场景HTTP 录音上传识别实现简单引擎兼容面广不能实时得等用户说完非实时质检、录音转写WebSocket/GRPC 流式识别实时性好交互自然需要自研流控与状态机语音机器人、实时对话FreeSWITCH 模块内直接接引擎延迟最低桥接最顺对引擎 SDK 侵入性强耦合高私有化、对延迟极度敏感我的项目里最推荐的是第二个方案FreeSWITCH 语音流在外面通过 WebSocket 推到本地或云上的 ASR 识别服务。原因是模块内直接嵌 SDK 虽然延迟漂亮但每次换引擎都要重新编译模块而且部分引擎并不提供 C/C SDK。WebSocket 流式识别则把 FreeSWITCH 侧的逻辑固定下来切引擎时只需要改外部服务的地址和协议细节。3.2 音频格式协商的坑FreeSWITCH 从 SIP 对端收到的音频可能是 G.711Aalaw、G.711Uulaw、G.729、opus 等格式。识别服务通常只认 16kHz 或 8kHz 的线性 PCML16。这个转换在 FreeSWITCH 上天然能完成因为它的核心媒体引擎具备转码能力。关键是你得知道 FreeSWITCH 内部的媒体处理模式。默认情况下allow_encoding和allow_decoding控制它是否对媒体做转码。如果你把 FreeSWITCH 当作 B2BUA背靠背用户代理它在收到 SIP 协商后的媒体格式后内部会按指定格式解码再按目标方向重新编码。实现 ASR 时一般是想要拿到解码后的原始 PCM 数据有两个办法在 Dialplan 里设置set: absolute_codec_stringPCMU,PCMA强制两端使用 G.711 编解码这样 FreeSWITCH 内部拿到的是简单的 L16 数据做了 G.711 转码但格式统一。用uuid_record或者media_bug挂一个录音/嗅探接口把一路媒体复制出来送到 ASR 服务这种方案不会干扰用户的正常通话链路。实际项目里我建议优先把握住旁路嗅探的思路。你不是要把通话吃掉而是在通话继续的同时复制一份音频给识别引擎。比如 FreeSWITCH 里的media_bug机制就是干这个的很多商业 ASR 对接模块也是基于这个原理来实现的。3.3 一个最小可跑的拨号计划示例下面的拨号计划演示了怎么在 FreeSWITCH 里建立一个本地分机呼叫同时把媒体转给外部 ASR 服务。这里用事件 socket 的方式把识别结果送回呼叫逻辑只是为了说明链路具体还是推荐你用模块方式接。extension nameasr_test condition fielddestination_number expression^8000$ action applicationanswer/ action applicationset dataasr_serverws://127.0.0.1:2700/ action applicationset dataasr_audio_rate16000/ action applicationstart_asr/ action applicationplayback dataivr/ivr-welcome.wav/ action applicationwait_for_asr_result/ action applicationplayback dataivr/ivr-you_said_$/${asr_text}.wav/ action applicationhangup/ /condition /extension这里start_asr和wait_for_asr_result是我基于 mod_asr 自定义的 app 接口功能是启动媒体嗅探并把识别结果写入asr_text变量。你实际包里的接口名字可能有差异但逻辑流程是一致的。4. FreeSWITCH ASR 模块的代码结构与实现如果你把 ASR 能力封装成 FreeSWITCH 原生模块可以让 Dialplan 直接调用这对业务来说最友好。下面是我在这个项目里实现模块时踩过的关键结构。4.1 模块对外暴露的接口FreeSWITCH 模块的基础是switch_loadable_module_interface_t。一个 ASR 模块通常会暴露两类接口Dialplan Application比如start_asr、stop_asr供号码流程里直接调用。事件/回调接口当识别引擎返回结果时模块把结果打包成事件或回调函数通知上层的呼叫控制逻辑。模块定义的骨架大概是这样SWITCH_MODULE_LOAD_FUNCTION(mod_asr_load); SWITCH_MODULE_SHUTDOWN_FUNCTION(mod_asr_shutdown); SWITCH_MODULE_DEFINITION(mod_asr, mod_asr_load, mod_asr_shutdown, NULL); static switch_status_t start_asr(switch_core_session_t *session, const char *data) { // 读取 data 里的引擎地址、采样率、语种等参数 // 初始化 media_bug 来获取通话音频 // 建立外部引擎的 websocket / grpc 通道 // 挂接音频帧回调函数 } static switch_status_t stop_asr(switch_core_session_t *session, const char *data) { // 发送结束帧等待最终识别结果 // 把结果写入 session 变量 }4.2 媒体采集的回调怎么写在 FreeSWITCH 里获取一路通话音频最标准的方法是注册一个switch_media_bug_t的回调让核心把读到的每个音频帧送过来。回调函数签名类似static switch_status_t asr_frame_callback(switch_media_bug_t *bug, const void *frame_data, switch_size_t frame_len, switch_frame_t *read_frame, switch_media_bug_frame_type_t type) { // 这里的 frame_data 就是解码后的 PCM 数据 // 根据协商的采样率决定是直接转发还是重采样后转发 // 通过 socket 发送到 ASR 引擎 }这里有个非常重要的细节type参数可能区分读方向和写方向。如果你只识别被叫用户的语音需要过滤掉主叫侧的音频帧否则会把机器人的播报音也一并识别进去。我实际的配置是在 start_asr 时传入方向参数回调里做判断。4.3 把识别结果变成可用的事件识别结果从引擎回传之后如果是最终结果我会直接写入switch_channel_set_variable让 Dialplan 后续动作能读取如果是中间结果我会以SWITCH_EVENT_CUSTOM的形式发出来上层的应用服务器通过 Event Socket 订阅实时转写内容。这样做的兼容性最高如果想做实时字幕、实时质检外部程序直接订阅事件即可不用改模块代码。switch_event_t *event; switch_event_create(event, SWITCH_EVENT_CUSTOM); event-subclass_name strdup(asr::result); switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, text, result_text); switch_event_add_header_string(event, SWITCH_STACK_BOTTOM, caller, caller_id); switch_event_fire(event); switch_event_free(event);比如座席助手场景里坐席电脑上可以实时显示用户说的话靠的就是这个事件订阅机制。5. ASR 引擎选型国内外主流方案对比模块写好后接下来的关键决策是选一个 ASR 引擎。项目标题既然叫 FreeSWITCH-ASR那引擎就类似一个可替换的电池。我把我实际测过的几个方案整理了一下给你做参考。5.1 国内引擎识别效果好但要注意私有化授权国内 ASR 引擎在中文识别上整体表现很强尤其是带口音的普通话、中英混说场景识别率明显比通用开源模型好。在电话语音场景里这些引擎一般会提供 WebSocket 流式识别接口和 FreeSWITCH 模块对接比较方便。国内引擎通常需要 API Key、签名鉴权。如果只是老实用公有云接口那部署最快但很多政府、金融客户要求全私有化购买私有化授权包的周期和费用就得提前纳入评估这经常是项目中最大的隐性成本。5.2 国外与开源引擎灵活可控但中文和口音处理需要调优国外开源方案以 Whisper 系、Kaldi 系以及各种基于 PyTorch 的端到端模型为主。比如 Whisper 在小样本、多语种场景下泛化能力很强但流式识别支持不是它的强项很多项目会做分段静音检测后用 Whisper 批量识别在对话型场景里延迟偏高。Kaldi 系比如 Chain 模型对流式识别支持相对更好也有成熟的端点检测VAD工具链但是配置复杂训练和模型更新需要一定的算法背景。如果团队没有专门的语音算法工程师纯开源路线在调优期会比较痛苦。5.3 实测选型对照表对比维度国内云厂商流式 ASR开源 Whisper 系开源 Kaldi 系中文识别准确率高自带口音优化中上需微调中依赖数据流式实时识别支持成熟一般需自建流式框架支持但工程复杂私有化部署成本贵授权费用高低硬件要求高低人力要求高开箱即用程度高中低与 FreeSWITCH 对接WebSocket 接口多可对接但需适配音频格式可对接但需自理 VAD 与流控我的个人建议是在业务验证阶段先用云厂商的流式 ASR 接口跑通整套流程确认产品形态没问题之后再考虑私有化或者开源替换。因为在 FreeSWITCH 侧不同的 ASR 引擎差异点是引擎适配层而你的模块、媒体流、事件体系可以保持一致。先保证链路跑通再优化准确率和成本这是比较理性的推进路线。6. 上线前后的常见问题排查清单把 FreeSWITCH 和 ASR 引擎跑起来不难难的是上线后各种怪问题。我把自己实际遇到过的几类问题整理成一份排查思路。6.1 方向识别结果把机器人的播报音也识别进去了现象用户没说话但系统识别出一长串文字内容正好是机器人在播放的提示语。排查链路先确认 FreeSWITCH 录音方向。播放提示音时音频会从 FreeSWITCH 往用户方向发送如果 ASR 模块没有区分读写方向就会把出向音频也截获送识别。查看模块回调里的type参数和read_frame的声道/方向信息加上方向过滤如果不想改代码就在业务层掐时间播报音播放完毕后再开启 ASR这样也能规避。6.2 延迟识别结果返回慢用户老是重复说喂现象一句话说完后系统要等两三秒才响应用户觉得机器人像没在听。排查链路延迟高一般有三个发力点。第一看 ASR 服务端的 VAD语音端点检测参数静音超时设太长会导致一直等用户把话说完再识别第二看音频采样率如果 FreeSWITCH 侧送的是 8kHz 音频而 ASR 模型是 16kHz 训练出来的识别服务要做重采样耗时必然增加第三看网络WebSocket 传输如果走公网且没有做协议缓冲抖动也会造成明显延迟。我的习惯是让 FreeSWITCH 侧直接按引擎要求的采样率发送减少一次服务器端转换。6.3 稳定性并发量上来后ASR 服务频繁断开现象测通时单路通话良好但一上压测十路并发之后 WebSocket 频繁断开FreeSWITCH 日志里面出现大量Socket timeout。排查链路先看 ASR 引擎侧的限制云厂商一般会限制单连接的最大音频并发时长和并发数超了就强制断开。然后看媒体发送的背压控制如果 FreeSWITCH 模块向服务端发送音频速度过快而服务端来不及处理客户端也会被断开。我当时的做法是模块里加了一个简单的发送队列同时根据引擎返回的音频帧限流信号调整发送节奏。如果用的是自建的 GPU 推理服务还得检查显存和推理 batch 的配置十路并发对 GPU 服务的影响远大于对 FreeSWITCH 本身的影响。6.4 字段内容识别结果有大量标点和重复文本现象ASR 返回结果里经常出现嗯啊然后然后之类口语词还有一些重复词。在业务意图识别里这些词会造成负面干扰。处理办法不要试图在 ASR 侧完全消除而是在上层做文本后处理。用正则剔除常见的语气词对连续重复的 token 做去重必要时再加一个文本纠错服务。我在项目里就写了一个轻量过滤器效果立竿见影。这类流程在 FreeSWITCH 架构里应该放在识别结果回来后的业务处理层不占用媒体通道改起来也方便。7. 基于 FreeSWITCH-ASR 的扩展思考做完整套 ASR 应用之后你会发现它更像一个中枢能延伸出不少有意思的方向。语音识别只是第一步后续可以做实体抽取、意图识别、情绪识别、对话管理甚至直接和 TTS 引擎串成一个完整的语音机器人闭环。FreeSWITCH 提供的play_and_detect_speech这类接口本质上就是在朝这个方向靠。如果你把 ASR 能力和 FreeSWITCH 的事件系统结合还可以做一些更细致的运营工具。比如对每一通通话打上实时标签监控是否有客户情绪激动、是否有敏感词出现这对呼叫中心和客服质检的价值非常大。我最后再分享一个经验在模块里把 ASR 的中间结果和最终结果都打上时间戳并和 FreeSWITCH 的 CDR呼叫详细记录关联起来后续做数据分析和问题回溯会很省力。我早期没做这个关联出了问题只能靠抓包后来补上了索引排查效率提高了不止一个量级。本文还有配套的精品资源点击获取