最近需要在本地部署一个语音识别服务,用于后续的音频转文字功能。经过对比后选择了阿里开源的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如果只是测试 FunASR,CPU 版本已经可以使用。
本次使用的 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 \ --privileged=true \ 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 2>&1 &这里使用了几个模型。
离线模型
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.py(GitHub官方源码)
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客户端输出:
ssl=1并且:
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 中的客户端就是先读取 WAV:
with 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_speaking=false ↓ 接收最终结果例如:
import websocket import json import wave def test_funasr(): ws = websocket.create_connection( "ws://127.0.0.1:10096", timeout=30 ) 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, opcode=websocket.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_ssl=False这一点在使用这个客户端时同样需要注意。
十六、funasr_client_api.py的另一个优点
这个客户端并不是把整个 WAV 文件直接发送给服务器。
它首先使用:
wave.open()读取音频,然后:
frames = wav_file.readframes( wav_file.getnframes() )获取 PCM 音频数据。
之后再计算:
stride将音频分成多个 chunk:
WAV ↓ 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进行验证,确认服务端本身工作正常之后,再根据自己的业务需求编写客户端。