From 15650e1a6c63e71c11b0fdbb6c376265804ab18f Mon Sep 17 00:00:00 2001 From: huozaimengli Date: Sun, 25 Jan 2026 11:27:34 +0800 Subject: [PATCH] =?UTF-8?q?refactor(asr):=20=E7=BB=9F=E4=B8=80=E9=9F=B3?= =?UTF-8?q?=E9=A2=91=E9=A2=84=E5=A4=84=E7=90=86=E9=80=BB=E8=BE=91=E5=B9=B6?= =?UTF-8?q?=E5=BC=95=E5=85=A5AudioArtifacts?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 重构所有ASR提供商的speech_to_text方法,将重复的音频解码、合并和文件保存逻辑提取到基类的speech_to_text_wrapper中。引入AudioArtifacts数据类封装PCM帧、字节数据、文件路径和临时路径,简化各提供商实现。移除各提供商中的冗余文件清理代码,由基类统一处理。 新增requires_file()和prefers_temp_file()方法允许提供商声明文件需求,优化内存和磁盘使用。保持接口兼容性的同时提高代码复用性和可维护性。 --- .../core/providers/asr/aliyun.py | 25 +--- .../core/providers/asr/baidu.py | 26 ++-- .../xiaozhi-server/core/providers/asr/base.py | 133 +++++++++++++++--- .../core/providers/asr/doubao.py | 24 +--- .../core/providers/asr/fun_local.py | 44 ++---- .../core/providers/asr/fun_server.py | 24 ++-- .../core/providers/asr/openai.py | 27 ++-- .../core/providers/asr/qwen3_asr_flash.py | 59 ++------ .../core/providers/asr/sherpa_onnx_local.py | 26 +--- .../core/providers/asr/tencent.py | 24 +--- .../xiaozhi-server/core/providers/asr/vosk.py | 38 ++--- 11 files changed, 190 insertions(+), 260 deletions(-) diff --git a/main/xiaozhi-server/core/providers/asr/aliyun.py b/main/xiaozhi-server/core/providers/asr/aliyun.py index 36f41745..60b0815a 100644 --- a/main/xiaozhi-server/core/providers/asr/aliyun.py +++ b/main/xiaozhi-server/core/providers/asr/aliyun.py @@ -220,29 +220,18 @@ class ASRProvider(ASRProviderBase): logger.warning("Token已过期,正在自动刷新...") self._refresh_token() - file_path = None try: - # 解码Opus为PCM - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - combined_pcm_data = b"".join(pcm_data) - - # 判断是否保存为WAV文件 - if self.delete_audio_file: - pass - else: - file_path = self.save_audio_to_file(pcm_data, session_id) - + artifacts = self.get_current_artifacts() + if artifacts is None: + return "", None # 发送请求并获取文本 - text = await self._send_request(combined_pcm_data) + text = await self._send_request(artifacts.pcm_bytes) if text: - return text, file_path + return text, artifacts.file_path - return "", file_path + return "", artifacts.file_path except Exception as e: logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) - return "", file_path + return "", None diff --git a/main/xiaozhi-server/core/providers/asr/baidu.py b/main/xiaozhi-server/core/providers/asr/baidu.py index aa8ea901..a808e4d5 100644 --- a/main/xiaozhi-server/core/providers/asr/baidu.py +++ b/main/xiaozhi-server/core/providers/asr/baidu.py @@ -37,30 +37,20 @@ class ASRProvider(ASRProviderBase): logger.bind(tag=TAG).warning("音频数据为空!") return None, None - file_path = None try: # 检查配置是否已设置 if not self.app_id or not self.api_key or not self.secret_key: logger.bind(tag=TAG).error("百度语音识别配置未设置,无法进行识别") - return None, file_path + return None, None - # 将Opus音频数据解码为PCM - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - combined_pcm_data = b"".join(pcm_data) - - # 判断是否保存为WAV文件 - if self.delete_audio_file: - pass - else: - self.save_audio_to_file(pcm_data, session_id) + artifacts = self.get_current_artifacts() + if artifacts is None: + return "", None start_time = time.time() # 识别本地文件 result = self.client.asr( - combined_pcm_data, + artifacts.pcm_bytes, "pcm", 16000, { @@ -73,13 +63,13 @@ class ASRProvider(ASRProviderBase): f"百度语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}" ) result = result["result"][0] - return result, file_path + return result, artifacts.file_path else: raise Exception( f"百度语音识别失败,错误码: {result['err_no']},错误信息: {result['err_msg']}" ) - return None, file_path + return None, artifacts.file_path except Exception as e: logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True) - return None, file_path + return None, None diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py index 0eb23d76..65922f81 100644 --- a/main/xiaozhi-server/core/providers/asr/base.py +++ b/main/xiaozhi-server/core/providers/asr/base.py @@ -8,14 +8,16 @@ import queue import asyncio import traceback import threading +import shutil import opuslib_next from abc import ABC, abstractmethod from config.logger import setup_logging -from typing import Optional, Tuple, List +from typing import Optional, Tuple, List, NamedTuple from core.handle.receiveAudioHandle import startToChat from core.handle.reportHandle import enqueue_asr_report from core.utils.util import remove_punctuation_and_length from core.handle.receiveAudioHandle import handleAudioMessage +import tempfile TAG = __name__ logger = setup_logging() @@ -23,7 +25,8 @@ logger = setup_logging() class ASRProviderBase(ABC): def __init__(self): - pass + self._current_artifacts: Optional[ASRProviderBase.AudioArtifacts] = None + """当前正在处理的音频 artifact""" # 打开音频通道 async def open_audio_channels(self, conn): @@ -93,10 +96,14 @@ class ASRProviderBase(ABC): wav_data = self._pcm_to_wav(combined_pcm_data) # 定义ASR任务 - asr_task = self.speech_to_text(asr_audio_task, conn.session_id, conn.audio_format) + asr_task = self.speech_to_text_wrapper( + asr_audio_task, conn.session_id, conn.audio_format + ) if conn.voiceprint_provider and wav_data: - voiceprint_task = conn.voiceprint_provider.identify_speaker(wav_data, conn.session_id) + voiceprint_task = conn.voiceprint_provider.identify_speaker( + wav_data, conn.session_id + ) # 并发等待两个结果 asr_result, voiceprint_result = await asyncio.gather( asr_task, voiceprint_task, return_exceptions=True @@ -160,19 +167,19 @@ class ASRProviderBase(ABC): # 使用自定义模块进行上报 await startToChat(conn, enhanced_text) enqueue_asr_report(conn, enhanced_text, asr_audio_task) - + except Exception as e: logger.bind(tag=TAG).error(f"处理语音停止失败: {e}") import traceback + logger.bind(tag=TAG).debug(f"异常详情: {traceback.format_exc()}") def _build_enhanced_text(self, text: str, speaker_name: Optional[str]) -> str: """构建包含说话人信息的文本(仅用于纯文本ASR)""" if speaker_name and speaker_name.strip(): - return json.dumps({ - "speaker": speaker_name, - "content": text - }, ensure_ascii=False) + return json.dumps( + {"speaker": speaker_name, "content": text}, ensure_ascii=False + ) else: return text @@ -181,23 +188,23 @@ class ASRProviderBase(ABC): if len(pcm_data) == 0: logger.bind(tag=TAG).warning("PCM数据为空,无法转换WAV") return b"" - + # 确保数据长度是偶数(16位音频) if len(pcm_data) % 2 != 0: pcm_data = pcm_data[:-1] - + # 创建WAV文件头 wav_buffer = io.BytesIO() try: - with wave.open(wav_buffer, 'wb') as wav_file: - wav_file.setnchannels(1) # 单声道 - wav_file.setsampwidth(2) # 16位 + with wave.open(wav_buffer, "wb") as wav_file: + wav_file.setnchannels(1) # 单声道 + wav_file.setsampwidth(2) # 16位 wav_file.setframerate(16000) # 16kHz采样率 wav_file.writeframes(pcm_data) - + wav_buffer.seek(0) wav_data = wav_buffer.read() - + return wav_data except Exception as e: logger.bind(tag=TAG).error(f"WAV转换失败: {e}") @@ -206,6 +213,37 @@ class ASRProviderBase(ABC): def stop_ws_connection(self): pass + class AudioArtifacts(NamedTuple): + pcm_frames: List[bytes] + pcm_bytes: bytes + file_path: Optional[str] + temp_path: Optional[str] + + def get_current_artifacts(self) -> Optional["ASRProviderBase.AudioArtifacts"]: + return self._current_artifacts + + def requires_file(self) -> bool: + """是否需要文件输入""" + return False + + def prefers_temp_file(self) -> bool: + """是否优先使用临时文件""" + return False + + def build_temp_file(self, pcm_bytes: bytes) -> Optional[str]: + try: + with tempfile.NamedTemporaryFile(suffix=".wav", delete=False) as temp_file: + temp_path = temp_file.name + with wave.open(temp_path, "wb") as wav_file: + wav_file.setnchannels(1) + wav_file.setsampwidth(2) + wav_file.setframerate(16000) + wav_file.writeframes(pcm_bytes) + return temp_path + except Exception as e: + logger.bind(tag=TAG).error(f"临时音频文件生成失败: {e}") + return None + def save_audio_to_file(self, pcm_data: List[bytes], session_id: str) -> str: """PCM数据保存为WAV文件""" module_name = __name__.split(".")[-1] @@ -220,6 +258,59 @@ class ASRProviderBase(ABC): return file_path + async def speech_to_text_wrapper( + self, opus_data: List[bytes], session_id: str, audio_format="opus" + ) -> Tuple[Optional[str], Optional[str]]: + file_path = None + temp_path = None + try: + if audio_format == "pcm": + pcm_data = opus_data + else: + pcm_data = self.decode_opus(opus_data) + combined_pcm_data = b"".join(pcm_data) + + free_space = shutil.disk_usage(self.output_dir).free + if free_space < len(combined_pcm_data) * 2: + raise OSError("磁盘空间不足") + + if self.requires_file() and self.prefers_temp_file(): + temp_path = self.build_temp_file(combined_pcm_data) + + if (hasattr(self, "delete_audio_file") and not self.delete_audio_file) or ( + self.requires_file() and not self.prefers_temp_file() + ): + file_path = self.save_audio_to_file(pcm_data, session_id) + + self._current_artifacts = ASRProviderBase.AudioArtifacts( + pcm_frames=pcm_data, + pcm_bytes=combined_pcm_data, + file_path=file_path, + temp_path=temp_path, + ) + + text, _ = await self.speech_to_text(opus_data, session_id, audio_format) + return text, file_path + except OSError as e: + logger.bind(tag=TAG).error(f"文件操作错误: {e}") + return None, None + except Exception as e: + logger.bind(tag=TAG).error(f"语音识别失败: {e}") + return None, None + finally: + try: + if temp_path and os.path.exists(temp_path): + os.unlink(temp_path) + if ( + hasattr(self, "delete_audio_file") + and self.delete_audio_file + and file_path + and os.path.exists(file_path) + ): + os.remove(file_path) + except Exception as e: + logger.bind(tag=TAG).error(f"文件清理失败: {e}") + @abstractmethod async def speech_to_text( self, opus_data: List[bytes], session_id: str, audio_format="opus" @@ -235,23 +326,23 @@ class ASRProviderBase(ABC): decoder = opuslib_next.Decoder(16000, 1) pcm_data = [] buffer_size = 960 # 每次处理960个采样点 (60ms at 16kHz) - + for i, opus_packet in enumerate(opus_data): try: if not opus_packet or len(opus_packet) == 0: continue - + pcm_frame = decoder.decode(opus_packet, buffer_size) if pcm_frame and len(pcm_frame) > 0: pcm_data.append(pcm_frame) - + except opuslib_next.OpusError as e: logger.bind(tag=TAG).warning(f"Opus解码错误,跳过数据包 {i}: {e}") except Exception as e: logger.bind(tag=TAG).error(f"音频处理错误,数据包 {i}: {e}") - + return pcm_data - + except Exception as e: logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}") return [] diff --git a/main/xiaozhi-server/core/providers/asr/doubao.py b/main/xiaozhi-server/core/providers/asr/doubao.py index f6cd79c6..a3ce9e35 100644 --- a/main/xiaozhi-server/core/providers/asr/doubao.py +++ b/main/xiaozhi-server/core/providers/asr/doubao.py @@ -236,20 +236,10 @@ class ASRProvider(ASRProviderBase): ) -> Tuple[Optional[str], Optional[str]]: """将语音数据转换为文本""" - file_path = None try: - # 合并所有opus数据包 - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - combined_pcm_data = b"".join(pcm_data) - - # 判断是否保存为WAV文件 - if self.delete_audio_file: - pass - else: - file_path = self.save_audio_to_file(pcm_data, session_id) + artifacts = self.get_current_artifacts() + if artifacts is None: + return "", None # 直接使用PCM数据 # 计算分段大小 (单声道, 16bit, 16kHz采样率) @@ -258,14 +248,14 @@ class ASRProvider(ASRProviderBase): # 语音识别 start_time = time.time() - text = await self._send_request(combined_pcm_data, segment_size) + text = await self._send_request(artifacts.pcm_bytes, segment_size) if text: logger.bind(tag=TAG).debug( f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text}" ) - return text, file_path - return "", file_path + return text, artifacts.file_path + return "", artifacts.file_path except Exception as e: logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) - return "", file_path + return "", None diff --git a/main/xiaozhi-server/core/providers/asr/fun_local.py b/main/xiaozhi-server/core/providers/asr/fun_local.py index bba53f8a..ee364d51 100644 --- a/main/xiaozhi-server/core/providers/asr/fun_local.py +++ b/main/xiaozhi-server/core/providers/asr/fun_local.py @@ -67,36 +67,19 @@ class ASRProvider(ASRProviderBase): self, opus_data: List[bytes], session_id: str, audio_format="opus" ) -> Tuple[Optional[str], Optional[str]]: """语音转文本主处理逻辑""" - file_path = None retry_count = 0 - + while retry_count < MAX_RETRIES: try: - # 合并所有opus数据包 - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - - combined_pcm_data = b"".join(pcm_data) - - # 检查磁盘空间 - if not self.delete_audio_file: - free_space = shutil.disk_usage(self.output_dir).free - if free_space < len(combined_pcm_data) * 2: # 预留2倍空间 - raise OSError("磁盘空间不足") - - # 判断是否保存为WAV文件 - if self.delete_audio_file: - pass - else: - file_path = self.save_audio_to_file(pcm_data, session_id) + artifacts = self.get_current_artifacts() + if artifacts is None: + return "", None # 语音识别 - 使用线程池避免阻塞事件循环 start_time = time.time() result = await asyncio.to_thread( self.model.generate, - input=combined_pcm_data, + input=artifacts.pcm_bytes, cache={}, language="auto", use_itn=True, @@ -107,7 +90,7 @@ class ASRProvider(ASRProviderBase): f"语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text['content']}" ) - return text, file_path + return text, artifacts.file_path except OSError as e: retry_count += 1 @@ -115,7 +98,7 @@ class ASRProvider(ASRProviderBase): logger.bind(tag=TAG).error( f"语音识别失败(已重试{retry_count}次): {e}", exc_info=True ) - return "", file_path + return "", None logger.bind(tag=TAG).warning( f"语音识别失败,正在重试({retry_count}/{MAX_RETRIES}): {e}" ) @@ -123,15 +106,4 @@ class ASRProvider(ASRProviderBase): except Exception as e: logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) - return "", file_path - - finally: - # 文件清理逻辑 - if self.delete_audio_file and file_path and os.path.exists(file_path): - try: - os.remove(file_path) - logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}") - except Exception as e: - logger.bind(tag=TAG).error( - f"文件删除失败: {file_path} | 错误: {e}" - ) + return "", None diff --git a/main/xiaozhi-server/core/providers/asr/fun_server.py b/main/xiaozhi-server/core/providers/asr/fun_server.py index c6674df9..cf169ef1 100644 --- a/main/xiaozhi-server/core/providers/asr/fun_server.py +++ b/main/xiaozhi-server/core/providers/asr/fun_server.py @@ -109,18 +109,10 @@ class ASRProvider(ASRProviderBase): :param session_id: Unique session identifier. :return: Tuple containing recognized text and optional timestamp. """ - file_path = None - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - combined_pcm_data = b"".join(pcm_data) - - # 判断是否保存为WAV文件 - if self.delete_audio_file: - pass - else: - file_path = self.save_audio_to_file(pcm_data, session_id) + artifacts = self.get_current_artifacts() + + if artifacts is None: + return "", None auth_header = {"Authorization": "Bearer; {}".format(self.api_key)} async with websockets.connect( self.uri, @@ -132,7 +124,7 @@ class ASRProvider(ASRProviderBase): try: # Use asyncio to handle WebSocket communication send_task = asyncio.create_task( - self._send_data(ws, combined_pcm_data, session_id) + self._send_data(ws, artifacts.pcm_bytes, session_id) ) receive_task = asyncio.create_task(self._receive_responses(ws)) @@ -161,14 +153,14 @@ class ASRProvider(ASRProviderBase): result = lang_tag_filter(result) return ( result, - file_path, + artifacts.file_path, ) # Return the recognized text and timestamp (if any) except websockets.exceptions.ConnectionClosed as e: logger.bind(tag=TAG).error(f"WebSocket connection closed: {e}") - return "", file_path + return "", artifacts.file_path except Exception as e: logger.bind(tag=TAG).error( f"Error during speech-to-text conversion: {e}", exc_info=True ) - return "", file_path + return "", artifacts.file_path diff --git a/main/xiaozhi-server/core/providers/asr/openai.py b/main/xiaozhi-server/core/providers/asr/openai.py index 5c6e849c..7e9ca7f3 100644 --- a/main/xiaozhi-server/core/providers/asr/openai.py +++ b/main/xiaozhi-server/core/providers/asr/openai.py @@ -21,20 +21,17 @@ class ASRProvider(ASRProviderBase): os.makedirs(self.output_dir, exist_ok=True) + def requires_file(self) -> bool: + return True + async def speech_to_text(self, opus_data: List[bytes], session_id: str, audio_format="opus") -> Tuple[Optional[str], Optional[str]]: file_path = None try: - start_time = time.time() - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - file_path = self.save_audio_to_file(pcm_data, session_id) - - logger.bind(tag=TAG).debug( - f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}" - ) - + artifacts = self.get_current_artifacts() + if artifacts is None: + return "", None + file_path = artifacts.file_path + logger.bind(tag=TAG).info(f"file path: {file_path}") headers = { "Authorization": f"Bearer {self.api_key}", @@ -71,12 +68,4 @@ class ASRProvider(ASRProviderBase): except Exception as e: logger.bind(tag=TAG).error(f"语音识别失败: {e}") return "", None - finally: - # 文件清理逻辑 - if self.delete_audio_file and file_path and os.path.exists(file_path): - try: - os.remove(file_path) - logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}") - except Exception as e: - logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}") diff --git a/main/xiaozhi-server/core/providers/asr/qwen3_asr_flash.py b/main/xiaozhi-server/core/providers/asr/qwen3_asr_flash.py index 51c84a37..aeb74703 100644 --- a/main/xiaozhi-server/core/providers/asr/qwen3_asr_flash.py +++ b/main/xiaozhi-server/core/providers/asr/qwen3_asr_flash.py @@ -1,5 +1,4 @@ import os -import tempfile from typing import Optional, Tuple, List import dashscope from config.logger import setup_logging @@ -35,27 +34,11 @@ class ASRProvider(ASRProviderBase): # 确保输出目录存在 os.makedirs(self.output_dir, exist_ok=True) - def _prepare_audio_file(self, pcm_data: bytes) -> str: - """将PCM数据转换为WAV文件并返回文件路径""" - try: - import wave - - # 创建临时WAV文件 - with tempfile.NamedTemporaryFile(suffix='.wav', delete=False) as temp_file: - temp_path = temp_file.name - - # 写入WAV格式 - with wave.open(temp_path, 'wb') as wav_file: - wav_file.setnchannels(1) # 单声道 - wav_file.setsampwidth(2) # 16位 - wav_file.setframerate(16000) # 16kHz采样率 - wav_file.writeframes(pcm_data) - - return temp_path - - except Exception as e: - logger.bind(tag=tag).error(f"音频文件准备失败: {e}") - return None + def prefers_temp_file(self) -> bool: + return True + + def requires_file(self) -> bool: + return True async def speech_to_text( self, opus_data: List[bytes], session_id: str, audio_format="opus" @@ -63,28 +46,14 @@ class ASRProvider(ASRProviderBase): """将语音数据转换为文本""" temp_file_path = None file_path = None - try: - # 解码音频数据 - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - - combined_pcm_data = b"".join(pcm_data) - if len(combined_pcm_data) == 0: - logger.bind(tag=tag).warning("音频数据为空") + artifacts = self.get_current_artifacts() + if artifacts is None: return "", None - - # 准备音频文件 - temp_file_path = self._prepare_audio_file(combined_pcm_data) + temp_file_path = artifacts.temp_path + file_path = artifacts.file_path if not temp_file_path: - return "", None - - # 保存音频文件(如果需要) - if not self.delete_audio_file: - file_path = self.save_audio_to_file(pcm_data, session_id) - + return "", file_path # 构造请求消息 messages = [ { @@ -141,11 +110,3 @@ class ASRProvider(ASRProviderBase): except Exception as e: logger.bind(tag=tag).error(f"语音识别失败: {e}") return "", file_path - - finally: - # 清理临时文件 - if temp_file_path and os.path.exists(temp_file_path): - try: - os.unlink(temp_file_path) - except Exception as e: - logger.bind(tag=tag).warning(f"清理临时文件失败: {e}") \ No newline at end of file diff --git a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py index c34909b2..44435773 100644 --- a/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py +++ b/main/xiaozhi-server/core/providers/asr/sherpa_onnx_local.py @@ -120,24 +120,20 @@ class ASRProvider(ASRProviderBase): samples_float32 = samples_float32 / 32768 return samples_float32, f.getframerate() + def requires_file(self) -> bool: + return True + async def speech_to_text( self, opus_data: List[bytes], session_id: str, audio_format="opus" ) -> Tuple[Optional[str], Optional[str]]: """语音转文本主处理逻辑""" file_path = None try: - # 保存音频文件 - start_time = time.time() - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - file_path = self.save_audio_to_file(pcm_data, session_id) - logger.bind(tag=TAG).debug( - f"音频文件保存耗时: {time.time() - start_time:.3f}s | 路径: {file_path}" - ) + artifacts = self.get_current_artifacts() + if artifacts is None: + return "", None + file_path = artifacts.file_path - # 语音识别 start_time = time.time() s = self.model.create_stream() samples, sample_rate = self.read_wave(file_path) @@ -153,11 +149,3 @@ class ASRProvider(ASRProviderBase): except Exception as e: logger.bind(tag=TAG).error(f"语音识别失败: {e}", exc_info=True) return "", file_path - finally: - # 文件清理逻辑 - if self.delete_audio_file and file_path and os.path.exists(file_path): - try: - os.remove(file_path) - logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}") - except Exception as e: - logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}") diff --git a/main/xiaozhi-server/core/providers/asr/tencent.py b/main/xiaozhi-server/core/providers/asr/tencent.py index 1fd1c6f4..e3554737 100644 --- a/main/xiaozhi-server/core/providers/asr/tencent.py +++ b/main/xiaozhi-server/core/providers/asr/tencent.py @@ -39,28 +39,18 @@ class ASRProvider(ASRProviderBase): logger.bind(tag=TAG).warning("音频数据为空!") return None, None - file_path = None try: # 检查配置是否已设置 if not self.secret_id or not self.secret_key: logger.bind(tag=TAG).error("腾讯云语音识别配置未设置,无法进行识别") - return None, file_path + return None, None - # 将Opus音频数据解码为PCM - if audio_format == "pcm": - pcm_data = opus_data - else: - pcm_data = self.decode_opus(opus_data) - combined_pcm_data = b"".join(pcm_data) - - # 判断是否保存为WAV文件 - if self.delete_audio_file: - pass - else: - self.save_audio_to_file(pcm_data, session_id) + artifacts = self.get_current_artifacts() + if artifacts is None: + return "", None # 将音频数据转换为Base64编码 - base64_audio = base64.b64encode(combined_pcm_data).decode("utf-8") + base64_audio = base64.b64encode(artifacts.pcm_bytes).decode("utf-8") # 构建请求体 request_body = self._build_request_body(base64_audio) @@ -77,11 +67,11 @@ class ASRProvider(ASRProviderBase): f"腾讯云语音识别耗时: {time.time() - start_time:.3f}s | 结果: {result}" ) - return result, file_path + return result, artifacts.file_path except Exception as e: logger.bind(tag=TAG).error(f"处理音频时发生错误!{e}", exc_info=True) - return None, file_path + return None, None def _build_request_body(self, base64_audio: str) -> str: """构建请求体""" diff --git a/main/xiaozhi-server/core/providers/asr/vosk.py b/main/xiaozhi-server/core/providers/asr/vosk.py index 19d31822..0f935c5d 100644 --- a/main/xiaozhi-server/core/providers/asr/vosk.py +++ b/main/xiaozhi-server/core/providers/asr/vosk.py @@ -44,36 +44,22 @@ class ASRProvider(ASRProviderBase): raise async def speech_to_text( - self, audio_data: List[bytes], session_id: str, audio_format: str = "opus" + self, opus_data: List[bytes], session_id: str, audio_format="opus" ) -> Tuple[Optional[str], Optional[str]]: """将语音数据转换为文本""" - file_path = None try: # 检查模型是否加载成功 if not self.model: logger.bind(tag=TAG).error("VOSK模型未加载,无法进行识别") return "", None - - # 解码音频(如果原始格式是Opus) - if audio_format == "pcm": - pcm_data = audio_data - else: - pcm_data = self.decode_opus(audio_data) - - if not pcm_data: - logger.bind(tag=TAG).warning("解码后的PCM数据为空,无法进行识别") + + artifacts = self.get_current_artifacts() + if artifacts is None: return "", None - - # 合并PCM数据 - combined_pcm_data = b"".join(pcm_data) - if len(combined_pcm_data) == 0: + if not artifacts.pcm_bytes: logger.bind(tag=TAG).warning("合并后的PCM数据为空") return "", None - # 判断是否保存为WAV文件 - if not self.delete_audio_file: - file_path = self.save_audio_to_file(pcm_data, session_id) - start_time = time.time() @@ -81,8 +67,8 @@ class ASRProvider(ASRProviderBase): chunk_size = 2000 text_result = "" - for i in range(0, len(combined_pcm_data), chunk_size): - chunk = combined_pcm_data[i:i+chunk_size] + for i in range(0, len(artifacts.pcm_bytes), chunk_size): + chunk = artifacts.pcm_bytes[i:i+chunk_size] if self.recognizer.AcceptWaveform(chunk): result = json.loads(self.recognizer.Result()) text = result.get('text', '') @@ -99,16 +85,8 @@ class ASRProvider(ASRProviderBase): f"VOSK语音识别耗时: {time.time() - start_time:.3f}s | 结果: {text_result.strip()}" ) - return text_result.strip(), file_path + return text_result.strip(), artifacts.file_path except Exception as e: logger.bind(tag=TAG).error(f"VOSK语音识别失败: {e}") return "", None - finally: - # 文件清理逻辑 - if self.delete_audio_file and file_path and os.path.exists(file_path): - try: - os.remove(file_path) - logger.bind(tag=TAG).debug(f"已删除临时音频文件: {file_path}") - except Exception as e: - logger.bind(tag=TAG).error(f"文件删除失败: {file_path} | 错误: {e}")