155 lines
5.3 KiB
Python
155 lines
5.3 KiB
Python
# funasr_client_api.py
|
||
import websocket
|
||
import threading
|
||
import time
|
||
import json
|
||
import ssl
|
||
from typing import Optional
|
||
|
||
|
||
class Funasr_websocket_recognizer:
|
||
def __init__(self, host: str, port: str, is_ssl: bool = False, mode: str = "offline",
|
||
chunk_size: str = "0,10,5", ping_interval: int = 30):
|
||
"""
|
||
FunASR WebSocket 识别器初始化
|
||
:param host: FunASR 服务端IP
|
||
:param port: FunASR 服务端端口
|
||
:param is_ssl: 是否使用SSL(wss)
|
||
:param mode: 识别模式(offline/online)
|
||
:param chunk_size: 分片参数(仅online模式有效)
|
||
:param ping_interval: 心跳间隔(秒)
|
||
"""
|
||
self.host = host
|
||
self.port = port
|
||
self.is_ssl = is_ssl
|
||
self.mode = mode
|
||
self.chunk_size = chunk_size
|
||
self.ping_interval = ping_interval
|
||
|
||
# WebSocket 连接配置
|
||
self.ws_url = f"{'wss' if is_ssl else 'ws'}://{host}:{port}/websocket/asr"
|
||
self.ws = None
|
||
self.ping_thread = None
|
||
self.stop_ping = False
|
||
self.result_buffer = []
|
||
self.lock = threading.Lock()
|
||
|
||
def _connect(self):
|
||
"""建立 WebSocket 连接"""
|
||
ssl_opt = {"cert_reqs": ssl.CERT_NONE} if self.is_ssl else None
|
||
self.ws = websocket.create_connection(self.ws_url, sslopt=ssl_opt)
|
||
|
||
# 发送初始化消息
|
||
init_msg = {
|
||
"mode": self.mode,
|
||
"chunk_size": self.chunk_size,
|
||
"format": "wav",
|
||
"rate": 16000,
|
||
"need_pun": True, # 是否需要标点
|
||
"encoding": "raw"
|
||
}
|
||
self.ws.send(json.dumps(init_msg))
|
||
|
||
# 启动心跳线程
|
||
self.stop_ping = False
|
||
self.ping_thread = threading.Thread(target=self._ping_worker)
|
||
self.ping_thread.daemon = True
|
||
self.ping_thread.start()
|
||
|
||
def _ping_worker(self):
|
||
"""心跳保活"""
|
||
while not self.stop_ping:
|
||
try:
|
||
if self.ws:
|
||
self.ws.ping()
|
||
time.sleep(self.ping_interval)
|
||
except Exception as e:
|
||
print(f"Ping 失败: {e}")
|
||
break
|
||
|
||
def feed_chunk(self, audio_data: bytes, wait_time: float = 0.02) -> Optional[str]:
|
||
"""
|
||
发送音频分片并获取识别结果
|
||
:param audio_data: 音频字节数据(16k 16bit 单声道)
|
||
:param wait_time: 等待结果的时间(秒)
|
||
:return: 实时识别文本(None 表示无结果)
|
||
"""
|
||
# 首次调用时建立连接
|
||
if not self.ws:
|
||
self._connect()
|
||
|
||
try:
|
||
# 发送音频数据(Base64 编码)
|
||
import base64
|
||
audio_b64 = base64.b64encode(audio_data).decode("utf-8")
|
||
data_msg = {
|
||
"data": audio_b64,
|
||
"is_last": False # 标记为非最后一包
|
||
}
|
||
self.ws.send(json.dumps(data_msg))
|
||
|
||
# 等待并读取结果
|
||
time.sleep(wait_time)
|
||
result = None
|
||
with self.lock:
|
||
while self.ws and self.ws.connected:
|
||
if self.ws.readyState == websocket.ABNF.READYSTATE_CLOSING:
|
||
break
|
||
try:
|
||
resp = self.ws.recv(timeout=0.01)
|
||
if resp:
|
||
resp_json = json.loads(resp)
|
||
if "text" in resp_json:
|
||
result = resp_json["text"]
|
||
self.result_buffer.append(result)
|
||
break
|
||
except websocket.WebSocketTimeoutException:
|
||
break
|
||
return result
|
||
except Exception as e:
|
||
print(f"发送分片失败: {e}")
|
||
return None
|
||
|
||
def close(self, timeout: float = 3) -> str:
|
||
"""
|
||
关闭连接并获取最终识别结果
|
||
:param timeout: 等待最终结果的超时时间(秒)
|
||
:return: 最终识别文本
|
||
"""
|
||
final_text = ""
|
||
try:
|
||
# 发送最后一包数据标记
|
||
if self.ws and self.ws.connected:
|
||
last_msg = {
|
||
"data": "",
|
||
"is_last": True
|
||
}
|
||
self.ws.send(json.dumps(last_msg))
|
||
|
||
# 等待最终结果
|
||
end_time = time.time() + timeout
|
||
while time.time() < end_time:
|
||
try:
|
||
resp = self.ws.recv(timeout=0.1)
|
||
if resp:
|
||
resp_json = json.loads(resp)
|
||
if "text" in resp_json:
|
||
final_text = resp_json["text"]
|
||
if resp_json.get("is_final", False):
|
||
break
|
||
except websocket.WebSocketTimeoutException:
|
||
continue
|
||
|
||
# 停止心跳并关闭连接
|
||
self.stop_ping = True
|
||
if self.ping_thread:
|
||
self.ping_thread.join(timeout=1)
|
||
if self.ws:
|
||
self.ws.close()
|
||
except Exception as e:
|
||
print(f"关闭连接失败: {e}")
|
||
|
||
# 合并所有结果
|
||
with self.lock:
|
||
all_text = "".join(self.result_buffer + [final_text])
|
||
return all_text |