FunASR Docker部署实战:搭建2Pass实时语音识别服务并完成WebSocket测试
最近需要在本地部署一个语音识别服务用于后续的音频转文字功能。经过对比后选择了阿里开源的FunASR。本文记录一次完整的 FunASR 部署过程包括Docker 部署 FunASR Runtime配置 Paraformer 离线模型配置 Online 实时模型配置 VAD、标点和 ITN启动 2Pass WebSocket 服务Python 客户端测试解决wss:///ws://导致的ConnectionResetError使用 FunASR 官方 WebSocket 客户端进行测试总结自己编写 WebSocket 客户端时需要注意的问题一、FunASR 简介FunASR 是阿里巴巴开源的一套语音识别工具包提供了语音识别、语音活动检测VAD、标点恢复、时间戳等能力。对于实际项目来说FunASR Runtime 提供了 WebSocket 服务可以让客户端通过 WebSocket 持续发送音频数据然后实时获取识别结果。其中比较值得关注的是2Pass模式。简单来说音频流 ↓ Online 实时识别 ↓ 快速返回实时结果 同时 ↓ Offline 离线识别 ↓ 对实时结果进行修正 ↓ 得到更准确的最终结果因此 2Pass 比单纯的 Online 实时识别更适合对实时性和准确率都有要求的场景。二、准备环境本次部署环境使用Ubuntu Docker FunASR Runtime CPU版本 Python如果只是测试 FunASRCPU 版本已经可以使用。本次使用的 Runtime 镜像为registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12这里需要注意FunASR Runtime 的版本比较多网上很多旧文章使用的是funasr-runtime-sdk-online-cpu-0.1.4如果按照旧教程部署部分客户端代码和参数可能与新版本存在差异。本文最终使用的是0.1.12三、创建模型目录首先创建一个目录保存 FunASR 的模型mkdir -p /mnt/data/funasr/models cd /mnt/data/funasr最终目录结构类似/mnt/data/funasr ├── models └── funasr_samplesDocker 启动的时候把宿主机的models挂载到容器宿主机 /mnt/data/funasr/models ↓ Docker Volume 容器 /workspace/models这样模型文件就不会随着 Docker 容器删除而丢失。四、拉取 FunASR Docker 镜像执行docker pull registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12查看镜像docker images | grep funasr应该可以看到类似registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr funasr-runtime-sdk-online-cpu-0.1.12五、启动 FunASR Docker 容器并进入交互模式执行docker run -it --rm \ --name funasr \ -p 10096:10095 \ -v $PWD/models:/workspace/models \ --privilegedtrue \ registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12 \ /bin/bash这里几个参数需要特别说明。1. 端口映射-p 10096:10095表示宿主机 10096 ↓ 容器 10095因此宿主机上的客户端应该连接127.0.0.1:10096而不是127.0.0.1:100952. 模型目录-v $PWD/models:/workspace/models表示把当前目录下的models挂载到/workspace/models后续 FunASR 下载的模型就可以保存在这里。六、进入 FunASR 容器上面的命令执行后已经进入容器。然后进入 Runtime 目录cd /workspace/FunASR/runtime查看文件ls这里可以看到 FunASR Runtime 相关脚本。七、启动2Pass WebSocket服务本次使用run_server_2pass.sh启动命令nohup bash run_server_2pass.sh \ --download-model-dir /workspace/models \ --model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx \ --online-model-dir damo/speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx \ --vad-dir damo/speech_fsmn_vad_zh-cn-16k-common-onnx \ --punc-dir damo/punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx \ --itn-dir thuduj12/fst_itn_zh \ --certfile 0 \ log.out 21 这里使用了几个模型。离线模型speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-onnx用于 Offline ASR。Online模型speech_paraformer-large_asr_nat-zh-cn-16k-common-vocab8404-online-onnx用于实时 Online ASR。VAD模型speech_fsmn_vad_zh-cn-16k-common-onnx用于检测语音开始和结束。标点模型punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx用于恢复标点。ITN模型fst_itn_zh用于逆文本规范化。八、为什么使用--certfile 0这里是整个部署过程中非常容易踩坑的地方。启动服务时--certfile 0意味着当前 WebSocket 服务没有启用 SSL。因此客户端应该使用ws://而不是wss://也就是说ws://127.0.0.1:10096是正确的。而wss://127.0.0.1:10096是不正确的。九、检查服务是否正常启动查看日志tail -f log.out或者docker exec -it funasr bash cd /workspace/FunASR/runtime tail -f log.out如果服务正常启动可以看到 Runtime 服务相关日志。也可以在宿主机检查端口ss -lntp | grep 10096应该能看到10096十、下载FunASR官方Python客户端FunASR 官方提供了 Python WebSocket 客户端可以直接用于测试 Runtime 服务。官方客户端源码funasr_wss_client.pyGitHub官方源码FunASR 官方 Runtime 快速开始文档FunASR Runtime 快速开始文档如果需要一次性下载官方 samples 测试工具也可以使用官方提供的压缩包wget https://isv-data.oss-cn-hangzhou.aliyuncs.com/ics/MaaS/ASR/sample/funasr_samples.tar.gz然后解压tar -zxvf funasr_samples.tar.gz进入 Python 客户端目录cd funasr_samples/samples/python其中可以找到funasr_wss_client.py funasr_client_api.py官方文档目前也是通过funasr_wss_client.py来测试 Runtime WebSocket 服务并支持offline、online和2pass模式。1. 安装客户端依赖funasr_wss_client.py使用 Python WebSocket 客户端因此首先安装pip install websockets如果使用的是官方较老版本的funasr_client_api.py则需要pip install websocket-client2. 使用官方客户端测试2Pass我们的 FunASR 服务运行在127.0.0.1:10096由于服务端启动时使用了--certfile 0没有开启 SSL所以客户端必须使用普通 WebSocket。执行python3 funasr_wss_client.py \ --host 127.0.0.1 \ --port 10096 \ --mode 2pass \ --audio_in 001_fixed.wav \ --ssl 0这里最重要的是--ssl 0官方客户端的--ssl参数默认值为1设置为0后使用普通ws://连接。正常情况下客户端会显示connect to ws://127.0.0.1:10096然后开始发送音频并接收识别结果。3. 为什么不能直接使用默认参数如果直接执行python3 funasr_wss_client.py \ --host 127.0.0.1 \ --port 10096 \ --mode 2pass \ --audio_in 001_fixed.wav客户端默认--ssl 1因此会连接wss://127.0.0.1:10096而我们的 FunASR 服务端使用--certfile 0关闭了 SSL因此服务端实际监听的是ws://127.0.0.1:10096两者协议不一致客户端 服务端 wss:// ───── TLS ──────×──── ws://最终会出现ConnectionResetError因此在关闭 SSL 的 FunASR Runtime 服务上测试时需要显式添加--ssl 04. 官方文档和客户端源码如果后续需要查看客户端支持的全部参数建议直接查看官方源码查看 funasr_wss_client.py 源码官方 Runtime 文档查看 FunASR Runtime 快速开始文档官方文档中的实时 2Pass 客户端示例也是python funasr_wss_client.py \ --host 127.0.0.1 \ --port 10095 \ --mode 2pass \ --chunk_size 5,10,5如果 Docker 将容器的10095映射到了宿主机的10096则把端口改成--port 10096即可。十一、第一次测试遇到的 ConnectionResetError最开始直接运行python3 funasr_wss_client.py \ --host 127.0.0.1 \ --port 10096 \ --mode 2pass \ --audio_in 001_fixed.wav客户端输出ssl1并且connect to wss://127.0.0.1:10096随后出现ConnectionResetError原因其实很明确客户端 ↓ wss:// ↓ TLS/SSL连接 × FunASR服务 ↓ ws:// ↓ 普通WebSocket两边的协议不一致。客户端尝试进行 TLS 握手而 FunASR 服务端并没有开启 TLS因此连接被服务端直接断开。十二、解决 ConnectionResetError只需要增加--ssl 0完整命令python3 funasr_wss_client.py \ --host 127.0.0.1 \ --port 10096 \ --mode 2pass \ --audio_in 001_fixed.wav \ --ssl 0此时客户端应该连接connect to ws://127.0.0.1:10096而不是connect to wss://127.0.0.1:10096这时候就可以正常进行 WebSocket 通信。十三、WAV文件需要注意什么FunASR Runtime 的 WebSocket 接口并不是简单地open(test.wav, rb).read()然后把整个 WAV 文件发送出去。WAV 文件结构大致是┌────────────────────┐ │ WAV Header │ ├────────────────────┤ │ PCM Audio Data │ ├────────────────────┤ │ PCM Audio Data │ ├────────────────────┤ │ ... │ └────────────────────┘WebSocket 客户端发送音频时应该发送其中的PCM Audio Data而不是完整的 WAV 文件。FunASR samples 中的客户端就是先读取 WAVwith wave.open(wav_path, rb) as wav_file: params wav_file.getparams() frames wav_file.readframes(wav_file.getnframes()) audio_bytes bytes(frames)然后再把audio_bytes分块发送给 WebSocket 服务。因此如果自己编写客户端不要简单写成with open(001_fixed.wav, rb) as f: audio_data f.read() ws.send(audio_data)否则可能导致服务端无法按照预期处理音频。十四、自己编写Python客户端如果不想使用官方的funasr_wss_client.py也可以自己实现。核心流程是读取WAV ↓ 提取PCM ↓ 建立WebSocket ↓ 发送JSON握手 ↓ 分块发送PCM ↓ 发送 is_speakingfalse ↓ 接收最终结果例如import websocket import json import wave def test_funasr(): ws websocket.create_connection( ws://127.0.0.1:10096, timeout30 ) print(WebSocket连接成功) with wave.open(001_fixed.wav, rb) as wf: channels wf.getnchannels() sample_width wf.getsampwidth() sample_rate wf.getframerate() pcm_data wf.readframes( wf.getnframes() ) print( channels:, channels, sample_width:, sample_width, sample_rate:, sample_rate ) # 建议音频格式 # 16kHz / 单声道 / 16bit PCM handshake { mode: 2pass, wav_name: test, audio_fs: 16000, is_speaking: True, itn: False } ws.send( json.dumps(handshake) ) # 根据实际项目需求分块发送 chunk_size 1920 for i in range( 0, len(pcm_data), chunk_size ): chunk pcm_data[ i:i chunk_size ] ws.send( chunk, opcodewebsocket.ABNF.OPCODE_BINARY ) # 告诉服务端音频发送结束 ws.send( json.dumps({ is_speaking: False }) ) try: while True: result ws.recv() if not result: break print( 识别结果:, result ) except websocket.WebSocketTimeoutException: print(等待识别结果超时) finally: ws.close() if __name__ __main__: test_funasr()实际生产环境中还需要按照 FunASR 2Pass 协议处理 Online、Offline 和最终结果而不是简单地把所有消息直接打印出来。十五、funasr_client_api.py是什么FunASR samples 中还有funasr_client_api.py这个文件名字很容易让人误以为它是 HTTP API 客户端。实际上从代码可以看到它仍然使用from websocket import create_connection建立 WebSocket 连接。它根据is_ssl决定使用wss://还是ws://代码逻辑是if is_ssl True: uri wss://{}:{}.format(host, port) else: uri ws://{}:{}.format(host, port)因此当前我们部署的--certfile 0对应is_sslFalse这一点在使用这个客户端时同样需要注意。十六、funasr_client_api.py的另一个优点这个客户端并不是把整个 WAV 文件直接发送给服务器。它首先使用wave.open()读取音频然后frames wav_file.readframes( wav_file.getnframes() )获取 PCM 音频数据。之后再计算stride将音频分成多个 chunkWAV ↓ PCM ↓ chunk 1 ↓ chunk 2 ↓ chunk 3 ↓ ... ↓ FunASR WebSocket这也是自己实现 FunASR WebSocket 客户端时非常值得参考的地方。十七、整个部署架构完成部署之后整体结构如下宿主机 ┌─────────────────────────────────────┐ │ │ │ Python Client │ │ │ │ │ │ WebSocket │ │ │ ws://127.0.0.1:10096 │ │ ▼ │ │ Docker Port │ │ │ │ │ │ 10096 → 10095 │ │ ▼ │ │ ┌───────────────────────────────┐ │ │ │ FunASR Container │ │ │ │ │ │ │ │ websocket-server-2pass │ │ │ │ │ │ │ │ │ ┌─────┴─────┐ │ │ │ │ │ │ │ │ │ │ Online Offline │ │ │ │ │ │ │ │ │ │ └─────┬─────┘ │ │ │ │ │ │ │ │ │ 2Pass │ │ │ │ │ │ │ │ │ 最终结果 │ │ │ └───────────────────────────────┘ │ │ │ │ /workspace/models │ │ ▲ │ └─────────────────┼───────────────────┘ │ /mnt/data/funasr/models十八、常见问题总结1.docker exec提示容器没有运行如果docker exec -it funasr bash提示container ... is not running说明容器启动后马上退出。首先不要反复执行docker run先查看docker ps -a然后docker logs funasr这通常可以直接找到容器退出原因。2.ConnectionResetError如果看到connect to wss://127.0.0.1:10096然后ConnectionResetError检查服务端是不是使用--certfile 0如果是那么客户端必须--ssl 0即ws://而不是wss://3. 10096和10095不要弄混Docker-p 10096:10095意味着宿主机10096 容器10095宿主机上的客户端127.0.0.1:10096容器内部的服务100954. 不要直接发送完整WAV不要简单open(test.wav, rb).read()然后ws.send(data)应该先提取PCM再按照 FunASR Runtime 的协议分块发送。十九、最终测试命令如果前面的服务已经启动最简单的测试方式就是cd funasr_samples/samples/python python3 funasr_wss_client.py \ --host 127.0.0.1 \ --port 10096 \ --mode 2pass \ --audio_in 001_fixed.wav \ --ssl 0如果能够正常输出识别结果就说明Docker ↓ FunASR Runtime ↓ Paraformer ↓ VAD ↓ 2Pass ↓ WebSocket ↓ Python Client整个链路已经打通。二十、后续项目集成完成 FunASR 部署后可以进一步把它集成到自己的业务系统中。例如浏览器 ↓ 上传音频 ↓ Next.js API ↓ Python ASR Service ↓ FunASR Runtime ↓ WebSocket ↓ Paraformer ↓ 返回文字如果只是普通的音频文件转文字可以考虑将 FunASR 封装成一个独立的 ASR 服务。如果需要实时语音转写则可以让前端通过 WebSocket 持续发送音频数据并使用 FunASR 的 Online Offline 2Pass 能力实现实时识别和最终结果修正。总结这次部署中最容易踩坑的其实不是 Docker而是FunASR WebSocket 协议和 SSL 配置。最关键的几个点可以总结成① Docker端口 10096:10095 ② 服务端没有启用SSL --certfile 0 ③ 客户端必须使用 ws:// 而不是 wss:// ④ WAV不要直接发送 提取PCM ⑤ 2Pass Online Offline 最终更准确的识别结果对于第一次部署 FunASR 的用户来说建议优先使用官方提供的funasr_wss_client.py进行验证确认服务端本身工作正常之后再根据自己的业务需求编写客户端。