ARTICLE DETAIL

资讯详情

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

FunASR与FreeSWITCH语音质检系统实战:Docker部署与WebSocket流式识别

FunASR与FreeSWITCH语音质检系统实战:Docker部署与WebSocket流式识别 1. 语音质检系统的整体架构与选型逻辑1.1 为什么要把 FunASR 和 FreeSWITCH 放在一起用做过呼叫中心质检的人都知道传统方案要么是录音落盘之后离线跑识别要么是买一套商业质检系统按坐席数付费。前者延迟高、反馈慢后者成本压不下来。我最早接触这个组合是因为一个中小型客服团队的需求他们每天产生大约 800 到 1200 通电话录音想做到“通话结束后 30 秒内出质检报告”同时预算有限不可能上商业方案。FreeSWITCH 在这里扮演的是通话控制与媒体流分发的角色。它本身是一个软交换机能处理 SIP 信令、管理通话桥接、把 RTP 媒体流引出来。而 FunASR 是阿里达摩院开源的一套语音识别工具链包含 Paraformer、SenseVoice 等模型中文识别准确率在开源方案里属于第一梯队而且支持流式识别和标点恢复。把两者接起来的关键点在于FreeSWITCH 通过mod_audio_fork或者mod_ws把音频流以 WebSocket 的方式推给后端服务后端服务再喂给 FunASR 做识别识别结果经过质检规则引擎打分最终落库生成报告。整条链路的核心协议就是WebSocket核心部署方式就是Docker。提示FreeSWITCH 原生支持 WebSocket 的方式有两种一种是通过mod_verto做信令层面的 WebSocket另一种是通过mod_audio_fork把媒体流 fork 出来。做语音质检要用后者别搞混了。1.2 整体数据流向拆解我把整条链路拆成五段方便你对照自己的环境排查问题通话接入层SIP 终端或运营商中继把通话接入 FreeSWITCHFreeSWITCH 建立通话桥接。媒体分流层通过mod_audio_fork在通话建立后把双向或单向音频以 16kHz、单声道、PCM 或 L16 格式 fork 到指定 WebSocket 地址。识别服务层后端 WebSocket 服务接收音频帧做重采样和缓冲调用 FunASR 的流式接口做实时识别。质检规则层识别出的文本按时间戳对齐跑关键词命中、语速检测、静音时长、情绪倾向等规则。存储与展示层质检结果写入数据库前端拉取报告。这个架构的好处是解耦。FreeSWITCH 只管通话和推流识别服务可以独立扩容质检规则可以随时改而不影响通话。坏处是链路长了之后排查问题需要逐段确认后面我会专门讲排查技巧。1.3 选型对比为什么不用其他方案方案识别准确率实时性部署成本中文支持FunASR FreeSWITCH高流式秒级低可自托管优秀商业质检系统高准实时高按坐席付费优秀离线录音 通用 ASR中分钟级中一般其他开源 ASR中低视模型而定低参差FunASR 的 Paraformer-large 模型在中文电话场景下的字错率CER实测能压到 5% 以内加上热词增强之后专有名词识别明显改善。这是它比通用 ASR 强的地方。2. 环境准备与 Docker 部署实操2.1 FreeSWITCH 的安装与关键配置FreeSWITCH 在 Linux 上的安装我建议直接用官方源或者编译安装。编译安装虽然慢但模块可控。以下是 Ubuntu 22.04 上的编译流程要点# 安装依赖 apt-get install -y build-essential autoconf automake libtool pkg-config \ libssl-dev libcurl4-openssl-dev libpcre3-dev libspeexdsp-dev \ libldns-dev libedit-dev libsqlite3-dev libopus-dev libsndfile1-dev # 拉取源码 git clone https://github.com/signalwire/freeswitch.git cd freeswitch ./bootstrap.sh -j ./configure --enable-portable-binary \ --with-openssl --with-pcre --with-speex --with-sqlite make -j$(nproc) make install编译完成之后mod_audio_fork不是默认编译的需要单独处理。这个模块在社区维护的仓库里拉下来放到src/mod/applications/下面然后在modules.conf里加上applications/mod_audio_fork重新make make install。配置方面autoload_configs/modules.conf.xml里要确保这几个模块加载load modulemod_sofia/ load modulemod_audio_fork/ load modulemod_event_socket/mod_audio_fork的用法是在 dialplan 里调用action applicationaudio_fork dataws://your-backend:9001/voice ws/这里的ws表示用 WebSocket 协议推流。如果你后端是 wss就写wss://。注意FreeSWITCH 默认的p-early-media-support参数会影响早期媒体处理做质检推流时建议在 SIP profile 里把它设为false避免在通话未完全建立时就开始推流导致音频错位。2.2 Docker 部署 FunASR 服务FunASR 官方提供了 Docker 镜像但实际用下来我建议自己写 Dockerfile因为要装一些额外的依赖比如ffmpeg做重采样、websockets库做服务端。FROM python:3.10-slim RUN apt-get update apt-get install -y \ ffmpeg libsndfile1 \ rm -rf /var/lib/apt/lists/* RUN pip install --no-cache-dir \ funasr \ modelscope \ websockets \ numpy \ torch --index-url https://download.pytorch.org/whl/cpu WORKDIR /app COPY server.py . CMD [python, server.py]这里有个坑torch 的 CPU 版本和 GPU 版本要分清。如果你的服务器没有 GPU装 GPU 版 torch 会白白占几个 G 的镜像体积启动还慢。用--index-url指定 CPU 源。模型下载方面FunASR 支持从 ModelScope 拉取。首次启动会自动下载 Paraformer-large 和标点模型大概 1.5G 左右。建议在 Dockerfile 里提前下载好或者挂载一个 volume 缓存模型目录避免每次重建容器都重新下载。from funasr import AutoModel model AutoModel( modelparaformer-zh, vad_modelfsmn-vad, punc_modelct-punc, devicecpu, disable_updateTrue )vad_model是语音活动检测用来切分长音频里的有效语音段做流式识别时这个很关键能避免把静音段也送进模型浪费算力。2.3 Docker Compose 编排整个链路单容器跑起来之后用 Docker Compose 把 FreeSWITCH、FunASR 服务、Nginx 串起来version: 3.8 services: freeswitch: image: freeswitch-custom:latest network_mode: host volumes: - ./freeswitch/conf:/usr/local/freeswitch/conf restart: unless-stopped funasr: build: ./funasr ports: - 9001:9001 volumes: - ./models:/root/.cache/modelscope restart: unless-stopped nginx: image: nginx:alpine ports: - 443:443 volumes: - ./nginx/nginx.conf:/etc/nginx/nginx.conf depends_on: - funasrFreeSWITCH 用network_mode: host是因为 SIP 和 RTP 对网络栈比较敏感NAT 环境下用 bridge 模式容易出单向音频的问题。Nginx 在这里的作用是反向代理 WebSocket把wss://your-domain/voice转发到ws://funasr:9001/voice。配置要点location /voice { proxy_pass http://funasr:9001; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Connection upgrade; proxy_read_timeout 3600s; proxy_send_timeout 3600s; }proxy_read_timeout一定要调大默认 60 秒通话超过一分钟就会被断开这个坑我踩过。3. WebSocket 音频流对接的核心实现3.1 后端 WebSocket 服务的骨架后端服务用 Python 的websockets库来写核心是一个异步处理函数。FreeSWITCH 推过来的音频是二进制帧格式默认是 16kHz 单声道 PCML16每帧 20ms也就是 320 个采样点640 字节。import asyncio import websockets import numpy as np from funasr import AutoModel model AutoModel( modelparaformer-zh, vad_modelfsmn-vad, punc_modelct-punc, devicecpu ) async def voice_socket(websocket): buffer bytearray() async for message in websocket: if isinstance(message, bytes): buffer.extend(message) # 每积累 1 秒音频做一次识别 if len(buffer) 32000: audio np.frombuffer(bytes(buffer), dtypenp.int16) buffer.clear() res model.generate( inputaudio, cache{}, is_finalFalse, chunk_size960 ) if res and res[0][text]: print(res[0][text]) else: # 文本帧可能是元数据 print(meta:, message) async def main(): async with websockets.serve(voice_socket, 0.0.0.0, 9001): await asyncio.Future() asyncio.run(main())这里的关键参数是chunk_size960对应 60ms 的音频块。FunASR 的流式识别需要按固定 chunk 喂数据chunk 太大延迟高太小识别效果差。960 是官方推荐值实测下来延迟和准确率平衡得比较好。3.2 音频格式转换与重采样FreeSWITCH 推流默认可能是 8kHz而 FunASR 的模型是 16kHz 训练的。如果不做重采样识别准确率会明显下降。我试过直接喂 8kHz 音频CER 从 5% 涨到 12% 左右。重采样用ffmpeg或者librosa都行但在流式场景下我建议用soxr库它支持流式重采样延迟低import soxr def resample_8k_to_16k(audio_int16): audio_float audio_int16.astype(np.float32) / 32768.0 resampled soxr.resample(audio_float, 8000, 16000) return (resampled * 32768.0).astype(np.int16)或者在 FreeSWITCH 侧直接配置推流采样率为 16kHzaction applicationset dataaudio_fork_sample_rate16000/ action applicationaudio_fork dataws://backend:9001/voice ws/这样后端就不用做重采样了省一层处理。但要注意如果 FreeSWITCH 的通道本身是 8kHz强行设 16kHz 会做一次上采样音质不会变好只是格式对齐。3.3 流式识别的缓存与断句处理FunASR 的流式识别依赖cache参数来维持上下文。每次调用generate时把上一次返回的 cache 传进去模型才能正确拼接上下文。如果每次都用空 cache识别结果会在 chunk 边界处断句错误。cache {} async for message in websocket: if isinstance(message, bytes): buffer.extend(message) if len(buffer) 32000: audio np.frombuffer(bytes(buffer), dtypenp.int16) buffer.clear() res model.generate( inputaudio, cachecache, is_finalFalse, chunk_size960 ) cache res[0][cache] text res[0][text]通话结束时FreeSWITCH 会发一个文本帧{event:close}之类的元数据这时候要调一次is_finalTrue把最后一段音频刷出来res model.generate( inputnp.array([], dtypenp.int16), cachecache, is_finalTrue, chunk_size960 )这个is_final调用很容易被忽略导致最后几个字丢失。我最早做的时候每通电话结尾都会少一两个字排查了半天才发现是没做 final flush。4. 质检规则引擎与结果落库4.1 质检规则的分类与实现识别出文本之后质检规则大致分四类关键词命中比如“投诉”“退款”“不知道”这类敏感词命中就扣分。语速检测按字数除以通话时长算过快或过慢都标记。静音检测VAD 输出的静音段超过阈值就标记可能是坐席离席或客户等待。情绪倾向用轻量情感分类模型对文本打分负面情绪标记。关键词命中用简单的字符串匹配或者 AC 自动机就行别上正则性能差。语速检测要注意识别文本的字数要按中文字符算标点不算。def check_speed(text, duration_sec): char_count len([c for c in text if \u4e00 c \u9fff]) if duration_sec 0: return 0 speed char_count / duration_sec * 60 # 字/分钟 if speed 300: return 过快 elif speed 100: return 过慢 return 正常正常语速大概在 180 到 260 字/分钟之间超过 300 基本就是在赶话低于 100 可能是客户在长篇陈述。4.2 识别结果与时间戳对齐FunASR 返回的结果里带时间戳的话需要开启return_raw_textTrue或者用带时间戳的模型。时间戳对齐的意义在于质检报告里能定位到具体哪一秒说了什么方便复核。res model.generate( inputaudio, cachecache, is_finalFalse, chunk_size960, return_raw_textTrue )如果模型不返回时间戳可以用 chunk 序号乘以 chunk 时长来估算。每个 chunk 是 60ms第 n 个 chunk 的起始时间就是 n * 0.06 秒。这个估算在流式场景下够用了。4.3 数据落库与报告生成质检结果建议存两张表一张通话记录表一张质检明细表。通话记录表存通话 ID、主被叫、开始结束时间、录音路径质检明细表存规则命中项、扣分、时间点。CREATE TABLE call_quality ( id BIGINT PRIMARY KEY AUTO_INCREMENT, call_id VARCHAR(64) NOT NULL, agent_id VARCHAR(32), start_time DATETIME, duration INT, total_score INT, detail JSON, created_at DATETIME DEFAULT CURRENT_TIMESTAMP, INDEX idx_call_id (call_id), INDEX idx_agent (agent_id) );detail字段用 JSON 存规则命中详情灵活改规则不用改表结构。MySQL 8.0 的 JSON 类型支持索引查询也方便。5. 常见问题与排查技巧实录5.1 WebSocket 连接建立失败这是最常见的问题表现是 FreeSWITCH 日志里报audio_fork: failed to connect。排查顺序确认后端服务在监听netstat -tlnp | grep 9001看端口有没有起来。确认网络可达从 FreeSWITCH 容器里curl一下后端地址注意容器网络模式。确认 Nginx 代理配置如果走了 Nginx检查Upgrade和Connection头有没有透传。确认协议匹配FreeSWITCH 写的是ws://还是wss://后端和 Nginx 要对应。我遇到过一次Nginx 配置里proxy_pass写成了http://而不是http://WebSocket 握手直接 400。虽然看起来都是 http但 WebSocket 升级请求必须走 HTTP/1.1 的 Upgrade 机制。5.2 音频断流或识别结果不完整表现是识别到一半没声音了或者每通电话结尾少字。原因通常有三个proxy_read_timeout太短Nginx 默认 60 秒通话超过一分钟就断。改成 3600s。没有做 final flush前面说的is_finalTrue调用必须加。缓冲区没清空如果 buffer 里剩的音频不足一个 chunk通话结束时没处理就会丢。排查方法是在后端服务里打日志记录每次收到的字节数和识别出的文本长度对比通话时长看哪里对不上。5.3 识别准确率不达预期如果 CER 明显偏高按这个顺序查排查项检查方法解决采样率不匹配打印音频 dtype 和采样率统一到 16kHz音频格式错误检查是 PCM 还是 L16转成 int16 PCM模型选错确认用的是 paraformer-zh换中文模型没有热词增强检查是否配置 hotword加行业热词VAD 切分过碎看 VAD 输出段数调 VAD 阈值热词增强对质检场景特别有用比如“工单”“派单”“超时赔付”这类词通用模型容易识别错。FunASR 支持在generate时传hotword参数res model.generate( inputaudio, cachecache, hotword工单 派单 超时赔付 退款, chunk_size960 )5.4 Docker 环境下的权限与网络问题Docker Desktop 在 Windows 上经常报virtualization support not detected这是 BIOS 里虚拟化没开或者 Hyper-V 和 WSL2 冲突。Linux 上则是权限问题普通用户不在 docker 组里每次都要 sudo。sudo usermod -aG docker $USER newgrp dockerFreeSWITCH 容器用 host 网络模式时要注意宿主机端口有没有被占用。5060 是 SIP 默认端口如果宿主机上已经跑了其他 SIP 服务会冲突。提示如果 FreeSWITCH 和 FunASR 不在同一台机器WebSocket 走公网时一定要上 wss并且 Nginx 配好证书。明文 ws 在公网上传音频安全和合规都说不过去。5.5 性能瓶颈与扩容思路单台 4 核 8G 的机器FunASR CPU 推理大概能扛 20 到 30 路并发流式识别。超过这个数识别延迟会明显上升。扩容有两个方向垂直扩容加 CPU 核数或者上 GPU。GPU 版 FunASR 单卡能扛 200 路以上。水平扩容起多个 FunASR 容器前面用 Nginx 做负载均衡。但要注意同一个通话的音频流必须打到同一个后端实例否则 cache 会乱。可以在 Nginx 里按call_id做一致性哈希。我实际用下来中小团队 50 路以内一台 8 核 16G 的机器跑 CPU 推理完全够用没必要上 GPU。GPU 的成本和运维复杂度对这个小规模来说不划算。5.6 质检规则误报的调优经验规则引擎刚上线时误报率通常很高。我的经验是关键词要加否定词过滤比如“不退款”命中了“退款”要加一层否定判断。语速阈值要分场景客服开场白和客户陈述的语速标准不一样别用同一个阈值。静音检测要排除等待音乐如果通话中有 hold 音乐VAD 会把它当静音要结合通话状态判断。调优是个持续过程建议先把规则阈值放宽收集一周数据看分布再逐步收紧。一上来就卡死阈值坐席会投诉误判。6. 从零到一的上线检查清单6.1 部署前的环境核对上线前把这几项过一遍能省掉很多半夜排查的功夫FreeSWITCH 版本和mod_audio_fork版本匹配编译无报错。FunASR 模型文件已缓存到本地容器重启不重新下载。Nginx WebSocket 代理配置已测试proxy_read_timeout大于最大通话时长。数据库表结构已建好索引已加。防火墙和端口映射已确认5060、9001、443 都通。6.2 灰度上线的节奏控制别一上来就全量推流。我的做法是先拿一路测试分机推流确认识别结果正确。扩到 10 路观察 CPU 和内存占用。扩到 50 路观察识别延迟和 WebSocket 断连率。全量上线同时保留离线录音兜底。每一步至少观察 24 小时确认稳定再进下一步。语音质检系统最怕的就是上线后大量断流坐席和质检员都会炸。6.3 日常运维的监控指标上线之后这几个指标要盯着指标正常范围异常处理WebSocket 断连率 1%查 Nginx 超时和网络识别延迟 2s查 CPU 负载和并发数识别 CER 8%查音频格式和模型质检规则命中率视业务而定查规则阈值容器内存占用 80%查模型缓存和泄漏监控用 Prometheus Grafana 就行后端服务暴露一个/metrics接口把识别次数、延迟、断连数打点。6.4 后续可扩展的方向这套架构跑通之后能扩展的地方不少。比如把识别文本接大模型做自动摘要生成通话摘要和坐席建议或者把质检结果接 BI 系统做坐席能力画像。FunASR 本身也支持说话人分离如果要做双声道质检可以开spk_model把坐席和客户的话分开识别规则命中会更精准。我在实际项目里最先加的就是说话人分离因为很多质检规则要区分是坐席说的还是客户说的。比如“投诉”这个词客户说和坐席说性质完全不一样。
返回列表