Files
aistream-test/python/test/funasr_client_api.py
2025-12-01 03:42:34 +08:00

155 lines
5.3 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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: 是否使用SSLwss
: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