:搭建2Pass實時語音識別服務并完成WebSocket測試)
最近需要在本地部署一個語音識別服務用于后續(xù)的音頻轉(zhuǎn)文字功能。經(jīng)過對比后選擇了阿里開源的FunASR。本文記錄一次完整的 FunASR 部署過程包括Docker 部署 FunASR Runtime配置 Paraformer 離線模型配置 Online 實時模型配置 VAD、標點和 ITN啟動 2Pass WebSocket 服務Python 客戶端測試解決wss:///ws://導致的ConnectionResetError使用 FunASR 官方 WebSocket 客戶端進行測試總結(jié)自己編寫 WebSocket 客戶端時需要注意的問題一、FunASR 簡介FunASR 是阿里巴巴開源的一套語音識別工具包提供了語音識別、語音活動檢測VAD、標點恢復、時間戳等能力。對于實際項目來說FunASR Runtime 提供了 WebSocket 服務可以讓客戶端通過 WebSocket 持續(xù)發(fā)送音頻數(shù)據(jù)然后實時獲取識別結(jié)果。其中比較值得關注的是2Pass模式。簡單來說音頻流 ↓ Online 實時識別 ↓ 快速返回實時結(jié)果 同時 ↓ Offline 離線識別 ↓ 對實時結(jié)果進行修正 ↓ 得到更準確的最終結(jié)果因此 2Pass 比單純的 Online 實時識別更適合對實時性和準確率都有要求的場景。二、準備環(huán)境本次部署環(huán)境使用Ubuntu Docker FunASR Runtime CPU版本 Python如果只是測試 FunASRCPU 版本已經(jīng)可以使用。本次使用的 Runtime 鏡像為registry.cn-hangzhou.aliyuncs.com/funasr_repo/funasr:funasr-runtime-sdk-online-cpu-0.1.12這里需要注意FunASR Runtime 的版本比較多網(wǎng)上很多舊文章使用的是funasr-runtime-sdk-online-cpu-0.1.4如果按照舊教程部署部分客戶端代碼和參數(shù)可能與新版本存在差異。本文最終使用的是0.1.12三、創(chuàng)建模型目錄首先創(chuàng)建一個目錄保存 FunASR 的模型mkdir -p /mnt/data/funasr/models cd /mnt/data/funasr最終目錄結(jié)構(gòu)類似/mnt/data/funasr ├── models └── funasr_samplesDocker 啟動的時候把宿主機的models掛載到容器宿主機 /mnt/data/funasr/models ↓ Docker Volume 容器 /workspace/models這樣模型文件就不會隨著 Docker 容器刪除而丟失。四、拉取 FunASR Docker 鏡像執(zhí)行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 容器并進入交互模式執(zhí)行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這里幾個參數(shù)需要特別說明。1. 端口映射-p 10096:10095表示宿主機 10096 ↓ 容器 10095因此宿主機上的客戶端應該連接127.0.0.1:10096而不是127.0.0.1:100952. 模型目錄-v $PWD/models:/workspace/models表示把當前目錄下的models掛載到/workspace/models后續(xù) FunASR 下載的模型就可以保存在這里。六、進入 FunASR 容器上面的命令執(zhí)行后已經(jīng)進入容器。然后進入 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用于檢測語音開始和結(jié)束。標點模型punc_ct-transformer_zh-cn-common-vad_realtime-vocab272727-onnx用于恢復標點。ITN模型fst_itn_zh用于逆文本規(guī)范化。八、為什么使用--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。執(zhí)行python3 funasr_wss_client.py \ --host 127.0.0.1 \ --port 10096 \ --mode 2pass \ --audio_in 001_fixed.wav \ --ssl 0這里最重要的是--ssl 0官方客戶端的--ssl參數(shù)默認值為1設置為0后使用普通ws://連接。正常情況下客戶端會顯示connect to ws://127.0.0.1:10096然后開始發(fā)送音頻并接收識別結(jié)果。3. 為什么不能直接使用默認參數(shù)如果直接執(zhí)行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因此服務端實際監(jiān)聽的是ws://127.0.0.1:10096兩者協(xié)議不一致客戶端 服務端 wss:// ───── TLS ──────×──── ws://最終會出現(xiàn)ConnectionResetError因此在關閉 SSL 的 FunASR Runtime 服務上測試時需要顯式添加--ssl 04. 官方文檔和客戶端源碼如果后續(xù)需要查看客戶端支持的全部參數(shù)建議直接查看官方源碼查看 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隨后出現(xiàn)ConnectionResetError原因其實很明確客戶端 ↓ wss:// ↓ TLS/SSL連接 × FunASR服務 ↓ ws:// ↓ 普通WebSocket兩邊的協(xié)議不一致??蛻舳藝L試進行 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 文件發(fā)送出去。WAV 文件結(jié)構(gòu)大致是┌────────────────────┐ │ WAV Header │ ├────────────────────┤ │ PCM Audio Data │ ├────────────────────┤ │ PCM Audio Data │ ├────────────────────┤ │ ... │ └────────────────────┘WebSocket 客戶端發(fā)送音頻時應該發(fā)送其中的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分塊發(fā)送給 WebSocket 服務。因此如果自己編寫客戶端不要簡單寫成with open(001_fixed.wav, rb) as f: audio_data f.read() ws.send(audio_data)否則可能導致服務端無法按照預期處理音頻。十四、自己編寫Python客戶端如果不想使用官方的funasr_wss_client.py也可以自己實現(xiàn)。核心流程是讀取WAV ↓ 提取PCM ↓ 建立WebSocket ↓ 發(fā)送JSON握手 ↓ 分塊發(fā)送PCM ↓ 發(fā)送 is_speakingfalse ↓ 接收最終結(jié)果例如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) ) # 根據(jù)實際項目需求分塊發(fā)送 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 ) # 告訴服務端音頻發(fā)送結(jié)束 ws.send( json.dumps({ is_speaking: False }) ) try: while True: result ws.recv() if not result: break print( 識別結(jié)果:, result ) except websocket.WebSocketTimeoutException: print(等待識別結(jié)果超時) finally: ws.close() if __name__ __main__: test_funasr()實際生產(chǎn)環(huán)境中還需要按照 FunASR 2Pass 協(xié)議處理 Online、Offline 和最終結(jié)果而不是簡單地把所有消息直接打印出來。十五、funasr_client_api.py是什么FunASR samples 中還有funasr_client_api.py這個文件名字很容易讓人誤以為它是 HTTP API 客戶端。實際上從代碼可以看到它仍然使用from websocket import create_connection建立 WebSocket 連接。它根據(jù)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的另一個優(yōu)點這個客戶端并不是把整個 WAV 文件直接發(fā)送給服務器。它首先使用wave.open()讀取音頻然后frames wav_file.readframes( wav_file.getnframes() )獲取 PCM 音頻數(shù)據(jù)。之后再計算stride將音頻分成多個 chunkWAV ↓ PCM ↓ chunk 1 ↓ chunk 2 ↓ chunk 3 ↓ ... ↓ FunASR WebSocket這也是自己實現(xiàn) FunASR WebSocket 客戶端時非常值得參考的地方。十七、整個部署架構(gòu)完成部署之后整體結(jié)構(gòu)如下宿主機 ┌─────────────────────────────────────┐ │ │ │ Python Client │ │ │ │ │ │ WebSocket │ │ │ ws://127.0.0.1:10096 │ │ ▼ │ │ Docker Port │ │ │ │ │ │ 10096 → 10095 │ │ ▼ │ │ ┌───────────────────────────────┐ │ │ │ FunASR Container │ │ │ │ │ │ │ │ websocket-server-2pass │ │ │ │ │ │ │ │ │ ┌─────┴─────┐ │ │ │ │ │ │ │ │ │ │ Online Offline │ │ │ │ │ │ │ │ │ │ └─────┬─────┘ │ │ │ │ │ │ │ │ │ 2Pass │ │ │ │ │ │ │ │ │ 最終結(jié)果 │ │ │ └───────────────────────────────┘ │ │ │ │ /workspace/models │ │ ▲ │ └─────────────────┼───────────────────┘ │ /mnt/data/funasr/models十八、常見問題總結(jié)1.docker exec提示容器沒有運行如果docker exec -it funasr bash提示container ... is not running說明容器啟動后馬上退出。首先不要反復執(zhí)行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容器內(nèi)部的服務100954. 不要直接發(fā)送完整WAV不要簡單open(test.wav, rb).read()然后ws.send(data)應該先提取PCM再按照 FunASR Runtime 的協(xié)議分塊發(fā)送。十九、最終測試命令如果前面的服務已經(jīng)啟動最簡單的測試方式就是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如果能夠正常輸出識別結(jié)果就說明Docker ↓ FunASR Runtime ↓ Paraformer ↓ VAD ↓ 2Pass ↓ WebSocket ↓ Python Client整個鏈路已經(jīng)打通。二十、后續(xù)項目集成完成 FunASR 部署后可以進一步把它集成到自己的業(yè)務系統(tǒng)中。例如瀏覽器 ↓ 上傳音頻 ↓ Next.js API ↓ Python ASR Service ↓ FunASR Runtime ↓ WebSocket ↓ Paraformer ↓ 返回文字如果只是普通的音頻文件轉(zhuǎn)文字可以考慮將 FunASR 封裝成一個獨立的 ASR 服務。如果需要實時語音轉(zhuǎn)寫則可以讓前端通過 WebSocket 持續(xù)發(fā)送音頻數(shù)據(jù)并使用 FunASR 的 Online Offline 2Pass 能力實現(xiàn)實時識別和最終結(jié)果修正??偨Y(jié)這次部署中最容易踩坑的其實不是 Docker而是FunASR WebSocket 協(xié)議和 SSL 配置。最關鍵的幾個點可以總結(jié)成① Docker端口 10096:10095 ② 服務端沒有啟用SSL --certfile 0 ③ 客戶端必須使用 ws:// 而不是 wss:// ④ WAV不要直接發(fā)送 提取PCM ⑤ 2Pass Online Offline 最終更準確的識別結(jié)果對于第一次部署 FunASR 的用戶來說建議優(yōu)先使用官方提供的funasr_wss_client.py進行驗證確認服務端本身工作正常之后再根據(jù)自己的業(yè)務需求編寫客戶端。