import json import uuid import asyncio import websockets from typing import List, TYPE_CHECKING if TYPE_CHECKING: from core.connection import ConnectionHandler from config.logger import setup_logging from core.providers.asr.base import ASRProviderBase from core.providers.asr.dto.dto import InterfaceType TAG = __name__ logger = setup_logging() class ASRProvider(ASRProviderBase): def __init__(self, config, delete_audio_file): super().__init__() self.interface_type = InterfaceType.STREAM self.config = config self.text = "" self.asr_ws = None self.forward_task = None self.is_processing = False self.server_ready = False # 服务器准备状态 self.task_id = None # 当前任务ID # 阿里百炼配置 self.api_key = config.get("api_key") self.model = config.get("model", "paraformer-realtime-v2") self.sample_rate = config.get("sample_rate", 16000) self.format = config.get("format", "pcm") # 可选参数 self.vocabulary_id = config.get("vocabulary_id") self.disfluency_removal_enabled = config.get("disfluency_removal_enabled", False) self.language_hints = config.get("language_hints") self.semantic_punctuation_enabled = config.get("semantic_punctuation_enabled", False) max_sentence_silence = config.get("max_sentence_silence") self.max_sentence_silence = int(max_sentence_silence) if max_sentence_silence else 200 self.multi_threshold_mode_enabled = config.get("multi_threshold_mode_enabled", False) self.punctuation_prediction_enabled = config.get("punctuation_prediction_enabled", True) self.inverse_text_normalization_enabled = config.get("inverse_text_normalization_enabled", True) # WebSocket URL self.ws_url = "wss://dashscope.aliyuncs.com/api-ws/v1/inference" self.output_dir = config.get("output_dir", "./audio_output") self.delete_audio_file = delete_audio_file async def open_audio_channels(self, conn): await super().open_audio_channels(conn) async def receive_audio(self, conn, pcm_frame, audio_have_voice): # 先调用父类方法处理基础逻辑 await super().receive_audio(conn, pcm_frame, audio_have_voice) # 只在有声音且没有连接时建立连接 if audio_have_voice and not self.is_processing and not self.asr_ws: try: await self._start_recognition(conn) except Exception as e: logger.bind(tag=TAG).error(f"开始识别失败: {str(e)}") await self._cleanup() return # 发送音频数据 if self.asr_ws and self.is_processing and self.server_ready: try: await self.asr_ws.send(pcm_frame) except Exception as e: logger.bind(tag=TAG).warning(f"发送音频失败: {str(e)}") await self._cleanup() async def _start_recognition(self, conn: "ConnectionHandler"): """开始识别会话""" try: # 如果为手动模式,设置超时时长为最大值 if conn.client_listen_mode == "manual": self.max_sentence_silence = 6000 self.is_processing = True self.task_id = uuid.uuid4().hex # 建立WebSocket连接 headers = { "Authorization": f"Bearer {self.api_key}" } logger.bind(tag=TAG).debug(f"正在连接阿里百炼ASR服务, task_id: {self.task_id}") self.asr_ws = await websockets.connect( self.ws_url, additional_headers=headers, max_size=1000000000, ping_interval=None, ping_timeout=None, close_timeout=5, ) logger.bind(tag=TAG).debug("WebSocket连接建立成功") self.server_ready = False self.forward_task = asyncio.create_task(self._forward_results(conn)) # 发送run-task指令 run_task_msg = self._build_run_task_message() await self.asr_ws.send(json.dumps(run_task_msg, ensure_ascii=False)) logger.bind(tag=TAG).debug("已发送run-task指令,等待服务器准备...") except Exception as e: logger.bind(tag=TAG).error(f"建立ASR连接失败: {str(e)}") if self.asr_ws: await self.asr_ws.close() self.asr_ws = None self.is_processing = False raise def _build_run_task_message(self) -> dict: """构建run-task指令""" message = { "header": { "action": "run-task", "task_id": self.task_id, "streaming": "duplex" }, "payload": { "task_group": "audio", "task": "asr", "function": "recognition", "model": self.model, "parameters": { "format": self.format, "sample_rate": self.sample_rate, "disfluency_removal_enabled": self.disfluency_removal_enabled, "semantic_punctuation_enabled": self.semantic_punctuation_enabled, "max_sentence_silence": self.max_sentence_silence, "multi_threshold_mode_enabled": self.multi_threshold_mode_enabled, "punctuation_prediction_enabled": self.punctuation_prediction_enabled, "inverse_text_normalization_enabled": self.inverse_text_normalization_enabled, }, "input": {} } } # 只有当模型名称以v2结尾时才添加vocabulary_id参数 if self.model.lower().endswith("v2"): message["payload"]["parameters"]["vocabulary_id"] = self.vocabulary_id if self.language_hints: message["payload"]["parameters"]["language_hints"] = self.language_hints return message async def _forward_results(self, conn: "ConnectionHandler"): """转发识别结果""" try: while not conn.stop_event.is_set(): # 获取当前连接的音频数据 audio_data = conn.asr_audio try: response = await asyncio.wait_for(self.asr_ws.recv(), timeout=1.0) result = json.loads(response) header = result.get("header", {}) payload = result.get("payload", {}) event = header.get("event", "") # 处理task-started事件 if event == "task-started": self.server_ready = True logger.bind(tag=TAG).debug("服务器已准备,开始发送缓存音频...") # 发送缓存音频 if conn.asr_audio: for cached_pcm in conn.asr_audio[-10:]: try: await self.asr_ws.send(cached_pcm) except Exception as e: logger.bind(tag=TAG).warning(f"发送缓存音频失败: {e}") break continue # 处理result-generated事件 elif event == "result-generated": output = payload.get("output", {}) sentence = output.get("sentence", {}) text = sentence.get("text", "") sentence_end = sentence.get("sentence_end", False) end_time = sentence.get("end_time") # 判断是否为最终结果(sentence_end为True且end_time不为null) is_final = sentence_end and end_time is not None if is_final: logger.bind(tag=TAG).info(f"识别到文本: {text}") # 手动模式下累积识别结果 if conn.client_listen_mode == "manual": if self.text: self.text += text else: self.text = text # 手动模式下,只有在收到stop信号后才触发处理 if conn.client_voice_stop: logger.bind(tag=TAG).debug("收到最终识别结果,触发处理") await self.handle_voice_stop(conn, audio_data) break else: # 自动模式下直接覆盖 self.text = text await self.handle_voice_stop(conn, audio_data) break # 处理task-finished事件 elif event == "task-finished": logger.bind(tag=TAG).debug("任务已完成") break # 处理task-failed事件 elif event == "task-failed": error_code = header.get("error_code", "UNKNOWN") error_message = header.get("error_message", "未知错误") logger.bind(tag=TAG).error(f"任务失败: {error_code} - {error_message}") break except asyncio.TimeoutError: continue except websockets.ConnectionClosed: logger.bind(tag=TAG).info("ASR服务连接已关闭") self.is_processing = False break except Exception as e: logger.bind(tag=TAG).error(f"处理结果失败: {str(e)}") break except Exception as e: logger.bind(tag=TAG).error(f"结果转发失败: {str(e)}") finally: # 清理连接的音频缓存 await self._cleanup() conn.reset_audio_states() async def _send_stop_request(self): """发送停止请求(用于手动模式停止录音)""" if self.asr_ws: try: # 先停止音频发送 self.is_processing = False logger.bind(tag=TAG).debug("收到停止请求,发送finish-task指令") await self._send_finish_task() except Exception as e: logger.bind(tag=TAG).error(f"发送停止请求失败: {e}") async def _send_finish_task(self): """发送finish-task指令""" if self.asr_ws and self.task_id: try: finish_msg = { "header": { "action": "finish-task", "task_id": self.task_id, "streaming": "duplex" }, "payload": { "input": {} } } await self.asr_ws.send(json.dumps(finish_msg, ensure_ascii=False)) logger.bind(tag=TAG).debug("已发送finish-task指令") except Exception as e: logger.bind(tag=TAG).error(f"发送finish-task指令失败: {e}") async def _cleanup(self): """清理资源""" logger.bind(tag=TAG).debug(f"开始ASR会话清理 | 当前状态: processing={self.is_processing}, server_ready={self.server_ready}") # 状态重置 self.is_processing = False self.server_ready = False logger.bind(tag=TAG).debug("ASR状态已重置") # 关闭连接 if self.asr_ws: try: # 先发送finish-task指令 await self._send_finish_task() # 等待一小段时间让服务器处理 await asyncio.sleep(0.1) logger.bind(tag=TAG).debug("正在关闭WebSocket连接") await asyncio.wait_for(self.asr_ws.close(), timeout=2.0) logger.bind(tag=TAG).debug("WebSocket连接已关闭") except Exception as e: logger.bind(tag=TAG).error(f"关闭WebSocket连接失败: {e}") finally: self.asr_ws = None # 清理任务引用 self.forward_task = None self.task_id = None logger.bind(tag=TAG).debug("ASR会话清理完成") async def speech_to_text(self, opus_data, session_id, artifacts=None): """获取识别结果""" result = self.text self.text = "" return result, None async def close(self): """关闭资源""" await self._cleanup()