From 5d439a4168c37a0c74c823e2da6f1d2586a57e91 Mon Sep 17 00:00:00 2001 From: 3030332422 <3030332422@qq.com> Date: Sun, 23 Nov 2025 11:55:38 +0800 Subject: [PATCH 1/2] =?UTF-8?q?update:=E5=86=85=E5=AD=98=E6=B3=84=E6=BC=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../core/handle/reportHandle.py | 68 ++++++++++--------- .../core/providers/asr/aliyun_stream.py | 6 +- .../xiaozhi-server/core/providers/asr/base.py | 6 ++ .../core/providers/asr/doubao_stream.py | 12 ++++ .../core/providers/asr/xunfei_stream.py | 12 ++++ .../core/providers/vad/silero.py | 6 ++ .../core/utils/opus_encoder_utils.py | 10 ++- main/xiaozhi-server/core/utils/util.py | 37 +++++----- 8 files changed, 107 insertions(+), 50 deletions(-) diff --git a/main/xiaozhi-server/core/handle/reportHandle.py b/main/xiaozhi-server/core/handle/reportHandle.py index 7b30f79c..09ad0218 100644 --- a/main/xiaozhi-server/core/handle/reportHandle.py +++ b/main/xiaozhi-server/core/handle/reportHandle.py @@ -10,7 +10,7 @@ TTS上报功能已集成到ConnectionHandler类中。 """ import time - +import gc import opuslib_next from config.manage_api_client import report as manage_report @@ -56,41 +56,47 @@ def opus_to_wav(conn, opus_data): Returns: bytes: WAV格式的音频数据 """ - decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 - pcm_data = [] + decoder = None + try: + decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道 + pcm_data = [] - for opus_packet in opus_data: - try: - pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms - pcm_data.append(pcm_frame) - except opuslib_next.OpusError as e: - conn.logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) + for opus_packet in opus_data: + try: + pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms + pcm_data.append(pcm_frame) + except opuslib_next.OpusError as e: + conn.logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True) - if not pcm_data: - raise ValueError("没有有效的PCM数据") + if not pcm_data: + raise ValueError("没有有效的PCM数据") - # 创建WAV文件头 - pcm_data_bytes = b"".join(pcm_data) - num_samples = len(pcm_data_bytes) // 2 # 16-bit samples + # 创建WAV文件头 + pcm_data_bytes = b"".join(pcm_data) + num_samples = len(pcm_data_bytes) // 2 # 16-bit samples - # WAV文件头 - wav_header = bytearray() - wav_header.extend(b"RIFF") # ChunkID - wav_header.extend((36 + len(pcm_data_bytes)).to_bytes(4, "little")) # ChunkSize - wav_header.extend(b"WAVE") # Format - wav_header.extend(b"fmt ") # Subchunk1ID - wav_header.extend((16).to_bytes(4, "little")) # Subchunk1Size - wav_header.extend((1).to_bytes(2, "little")) # AudioFormat (PCM) - wav_header.extend((1).to_bytes(2, "little")) # NumChannels - wav_header.extend((16000).to_bytes(4, "little")) # SampleRate - wav_header.extend((32000).to_bytes(4, "little")) # ByteRate - wav_header.extend((2).to_bytes(2, "little")) # BlockAlign - wav_header.extend((16).to_bytes(2, "little")) # BitsPerSample - wav_header.extend(b"data") # Subchunk2ID - wav_header.extend(len(pcm_data_bytes).to_bytes(4, "little")) # Subchunk2Size + # WAV文件头 + wav_header = bytearray() + wav_header.extend(b"RIFF") # ChunkID + wav_header.extend((36 + len(pcm_data_bytes)).to_bytes(4, "little")) # ChunkSize + wav_header.extend(b"WAVE") # Format + wav_header.extend(b"fmt ") # Subchunk1ID + wav_header.extend((16).to_bytes(4, "little")) # Subchunk1Size + wav_header.extend((1).to_bytes(2, "little")) # AudioFormat (PCM) + wav_header.extend((1).to_bytes(2, "little")) # NumChannels + wav_header.extend((16000).to_bytes(4, "little")) # SampleRate + wav_header.extend((32000).to_bytes(4, "little")) # ByteRate + wav_header.extend((2).to_bytes(2, "little")) # BlockAlign + wav_header.extend((16).to_bytes(2, "little")) # BitsPerSample + wav_header.extend(b"data") # Subchunk2ID + wav_header.extend(len(pcm_data_bytes).to_bytes(4, "little")) # Subchunk2Size - # 返回完整的WAV数据 - return bytes(wav_header) + pcm_data_bytes + # 返回完整的WAV数据 + return bytes(wav_header) + pcm_data_bytes + finally: + if decoder: + del decoder + gc.collect() def enqueue_tts_report(conn, text, opus_data): diff --git a/main/xiaozhi-server/core/providers/asr/aliyun_stream.py b/main/xiaozhi-server/core/providers/asr/aliyun_stream.py index 8acc640a..0728f6c6 100644 --- a/main/xiaozhi-server/core/providers/asr/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/asr/aliyun_stream.py @@ -5,6 +5,7 @@ import hmac import base64 import hashlib import asyncio +import gc import requests import websockets import opuslib_next @@ -347,4 +348,7 @@ class ASRProvider(ASRProviderBase): async def close(self): """关闭资源""" - await self._cleanup() + await self._cleanup(None) + if hasattr(self, 'decoder'): + del self.decoder + gc.collect() diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py index ed69da89..d0a26cf7 100644 --- a/main/xiaozhi-server/core/providers/asr/base.py +++ b/main/xiaozhi-server/core/providers/asr/base.py @@ -10,6 +10,7 @@ import traceback import threading import opuslib_next import concurrent.futures +import gc from abc import ABC, abstractmethod from config.logger import setup_logging from typing import Optional, Tuple, List @@ -241,6 +242,7 @@ class ASRProviderBase(ABC): @staticmethod def decode_opus(opus_data: List[bytes]) -> List[bytes]: """将Opus音频数据解码为PCM数据""" + decoder = None try: decoder = opuslib_next.Decoder(16000, 1) pcm_data = [] @@ -265,3 +267,7 @@ class ASRProviderBase(ABC): except Exception as e: logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}") return [] + finally: + if decoder: + del decoder + gc.collect() diff --git a/main/xiaozhi-server/core/providers/asr/doubao_stream.py b/main/xiaozhi-server/core/providers/asr/doubao_stream.py index 67964075..ed1c0c9d 100644 --- a/main/xiaozhi-server/core/providers/asr/doubao_stream.py +++ b/main/xiaozhi-server/core/providers/asr/doubao_stream.py @@ -4,6 +4,7 @@ import uuid import asyncio import websockets import opuslib_next +import gc from core.providers.asr.base import ASRProviderBase from config.logger import setup_logging from core.providers.asr.dto.dto import InterfaceType @@ -370,6 +371,17 @@ class ASRProvider(ASRProviderBase): pass self.forward_task = None self.is_processing = False + + # 显式释放decoder资源 + if hasattr(self, 'decoder') and self.decoder: + try: + del self.decoder + self.decoder = None + gc.collect() + logger.bind(tag=TAG).info("Doubao decoder resources released") + except Exception as e: + logger.bind(tag=TAG).error(f"Error releasing Doubao decoder: {e}") + # 清理所有连接的音频缓冲区 if hasattr(self, '_connections'): for conn in self._connections.values(): diff --git a/main/xiaozhi-server/core/providers/asr/xunfei_stream.py b/main/xiaozhi-server/core/providers/asr/xunfei_stream.py index e91c8f09..0022d693 100644 --- a/main/xiaozhi-server/core/providers/asr/xunfei_stream.py +++ b/main/xiaozhi-server/core/providers/asr/xunfei_stream.py @@ -5,6 +5,7 @@ import hashlib import asyncio import websockets import opuslib_next +import gc from time import mktime from datetime import datetime from urllib.parse import urlencode @@ -512,6 +513,17 @@ class ASRProvider(ASRProviderBase): pass self.forward_task = None self.is_processing = False + + # 显式释放decoder资源 + if hasattr(self, 'decoder') and self.decoder: + try: + del self.decoder + self.decoder = None + gc.collect() + logger.bind(tag=TAG).info("Xunfei decoder resources released") + except Exception as e: + logger.bind(tag=TAG).error(f"Error releasing Xunfei decoder: {e}") + # 清理所有连接的音频缓冲区 if hasattr(self, "_connections"): for conn in self._connections.values(): diff --git a/main/xiaozhi-server/core/providers/vad/silero.py b/main/xiaozhi-server/core/providers/vad/silero.py index 2263fcb7..738c2646 100644 --- a/main/xiaozhi-server/core/providers/vad/silero.py +++ b/main/xiaozhi-server/core/providers/vad/silero.py @@ -2,6 +2,7 @@ import time import numpy as np import torch import opuslib_next +import gc from config.logger import setup_logging from core.providers.vad.base import VADProviderBase @@ -36,6 +37,11 @@ class VADProvider(VADProviderBase): # 至少要多少帧才算有语音 self.frame_window_threshold = 3 + def __del__(self): + if hasattr(self, 'decoder'): + del self.decoder + gc.collect() + def is_vad(self, conn, opus_packet): try: pcm_frame = self.decoder.decode(opus_packet, 960) diff --git a/main/xiaozhi-server/core/utils/opus_encoder_utils.py b/main/xiaozhi-server/core/utils/opus_encoder_utils.py index ae7066ce..cf76dd6b 100644 --- a/main/xiaozhi-server/core/utils/opus_encoder_utils.py +++ b/main/xiaozhi-server/core/utils/opus_encoder_utils.py @@ -6,6 +6,7 @@ Opus编码工具类 import logging import traceback import numpy as np +import gc from opuslib_next import Encoder from opuslib_next import constants from typing import Optional, Callable, Any @@ -128,5 +129,10 @@ class OpusEncoderUtils: def close(self): """关闭编码器并释放资源""" - # opuslib没有明确的关闭方法,Python的垃圾回收会处理 - pass \ No newline at end of file + if hasattr(self, 'encoder') and self.encoder: + try: + del self.encoder + self.encoder = None + gc.collect() + except Exception as e: + logging.error(f"Error releasing Opus encoder: {e}") \ No newline at end of file diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py index f10cc369..190c4d6a 100644 --- a/main/xiaozhi-server/core/utils/util.py +++ b/main/xiaozhi-server/core/utils/util.py @@ -8,6 +8,7 @@ import requests import subprocess import numpy as np import opuslib_next +import gc from io import BytesIO from core.utils import p3 from pydub import AudioSegment @@ -372,26 +373,30 @@ def opus_datas_to_wav_bytes(opus_datas, sample_rate=16000, channels=1): 将opus帧列表解码为wav字节流 """ decoder = opuslib_next.Decoder(sample_rate, channels) - pcm_datas = [] + try: + pcm_datas = [] - frame_duration = 60 # ms - frame_size = int(sample_rate * frame_duration / 1000) # 960 + frame_duration = 60 # ms + frame_size = int(sample_rate * frame_duration / 1000) # 960 - for opus_frame in opus_datas: - # 解码为PCM(返回bytes,2字节/采样点) - pcm = decoder.decode(opus_frame, frame_size) - pcm_datas.append(pcm) + for opus_frame in opus_datas: + # 解码为PCM(返回bytes,2字节/采样点) + pcm = decoder.decode(opus_frame, frame_size) + pcm_datas.append(pcm) - pcm_bytes = b"".join(pcm_datas) + pcm_bytes = b"".join(pcm_datas) - # 写入wav字节流 - wav_buffer = BytesIO() - with wave.open(wav_buffer, "wb") as wf: - wf.setnchannels(channels) - wf.setsampwidth(2) # 16bit - wf.setframerate(sample_rate) - wf.writeframes(pcm_bytes) - return wav_buffer.getvalue() + # 写入wav字节流 + wav_buffer = BytesIO() + with wave.open(wav_buffer, "wb") as wf: + wf.setnchannels(channels) + wf.setsampwidth(2) # 16bit + wf.setframerate(sample_rate) + wf.writeframes(pcm_bytes) + return wav_buffer.getvalue() + finally: + del decoder + gc.collect() def check_vad_update(before_config, new_config): From 83a3d0eabedeae1c2ce1143df5bf8f2812b3f5b0 Mon Sep 17 00:00:00 2001 From: hrz <1710360675@qq.com> Date: Sun, 23 Nov 2025 15:11:35 +0800 Subject: [PATCH 2/2] =?UTF-8?q?update:=E4=BC=98=E5=8C=96=E4=BB=A3=E7=A0=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- main/xiaozhi-server/core/handle/reportHandle.py | 9 ++++++--- .../core/providers/asr/aliyun_stream.py | 11 ++++++++--- main/xiaozhi-server/core/providers/asr/base.py | 9 ++++++--- .../core/providers/asr/doubao_stream.py | 6 +++--- .../core/providers/asr/xunfei_stream.py | 6 +++--- main/xiaozhi-server/core/providers/vad/silero.py | 9 ++++++--- main/xiaozhi-server/core/utils/util.py | 8 ++++++-- 7 files changed, 38 insertions(+), 20 deletions(-) diff --git a/main/xiaozhi-server/core/handle/reportHandle.py b/main/xiaozhi-server/core/handle/reportHandle.py index 09ad0218..cc78a157 100644 --- a/main/xiaozhi-server/core/handle/reportHandle.py +++ b/main/xiaozhi-server/core/handle/reportHandle.py @@ -94,9 +94,12 @@ def opus_to_wav(conn, opus_data): # 返回完整的WAV数据 return bytes(wav_header) + pcm_data_bytes finally: - if decoder: - del decoder - gc.collect() + if decoder is not None: + try: + del decoder + gc.collect() + except Exception as e: + conn.logger.bind(tag=TAG).debug(f"释放decoder资源时出错: {e}") def enqueue_tts_report(conn, text, opus_data): diff --git a/main/xiaozhi-server/core/providers/asr/aliyun_stream.py b/main/xiaozhi-server/core/providers/asr/aliyun_stream.py index 0728f6c6..23999fca 100644 --- a/main/xiaozhi-server/core/providers/asr/aliyun_stream.py +++ b/main/xiaozhi-server/core/providers/asr/aliyun_stream.py @@ -349,6 +349,11 @@ class ASRProvider(ASRProviderBase): async def close(self): """关闭资源""" await self._cleanup(None) - if hasattr(self, 'decoder'): - del self.decoder - gc.collect() + if hasattr(self, 'decoder') and self.decoder is not None: + try: + del self.decoder + self.decoder = None + gc.collect() + logger.bind(tag=TAG).debug("Aliyun decoder resources released") + except Exception as e: + logger.bind(tag=TAG).debug(f"释放Aliyun decoder资源时出错: {e}") diff --git a/main/xiaozhi-server/core/providers/asr/base.py b/main/xiaozhi-server/core/providers/asr/base.py index d0a26cf7..2d88ff70 100644 --- a/main/xiaozhi-server/core/providers/asr/base.py +++ b/main/xiaozhi-server/core/providers/asr/base.py @@ -268,6 +268,9 @@ class ASRProviderBase(ABC): logger.bind(tag=TAG).error(f"音频解码过程发生错误: {e}") return [] finally: - if decoder: - del decoder - gc.collect() + if decoder is not None: + try: + del decoder + gc.collect() + except Exception as e: + logger.bind(tag=TAG).debug(f"释放decoder资源时出错: {e}") diff --git a/main/xiaozhi-server/core/providers/asr/doubao_stream.py b/main/xiaozhi-server/core/providers/asr/doubao_stream.py index ed1c0c9d..7d0871a6 100644 --- a/main/xiaozhi-server/core/providers/asr/doubao_stream.py +++ b/main/xiaozhi-server/core/providers/asr/doubao_stream.py @@ -373,14 +373,14 @@ class ASRProvider(ASRProviderBase): self.is_processing = False # 显式释放decoder资源 - if hasattr(self, 'decoder') and self.decoder: + if hasattr(self, 'decoder') and self.decoder is not None: try: del self.decoder self.decoder = None gc.collect() - logger.bind(tag=TAG).info("Doubao decoder resources released") + logger.bind(tag=TAG).debug("Doubao decoder resources released") except Exception as e: - logger.bind(tag=TAG).error(f"Error releasing Doubao decoder: {e}") + logger.bind(tag=TAG).debug(f"释放Doubao decoder资源时出错: {e}") # 清理所有连接的音频缓冲区 if hasattr(self, '_connections'): diff --git a/main/xiaozhi-server/core/providers/asr/xunfei_stream.py b/main/xiaozhi-server/core/providers/asr/xunfei_stream.py index 0022d693..7bd992a9 100644 --- a/main/xiaozhi-server/core/providers/asr/xunfei_stream.py +++ b/main/xiaozhi-server/core/providers/asr/xunfei_stream.py @@ -515,14 +515,14 @@ class ASRProvider(ASRProviderBase): self.is_processing = False # 显式释放decoder资源 - if hasattr(self, 'decoder') and self.decoder: + if hasattr(self, 'decoder') and self.decoder is not None: try: del self.decoder self.decoder = None gc.collect() - logger.bind(tag=TAG).info("Xunfei decoder resources released") + logger.bind(tag=TAG).debug("Xunfei decoder resources released") except Exception as e: - logger.bind(tag=TAG).error(f"Error releasing Xunfei decoder: {e}") + logger.bind(tag=TAG).debug(f"释放Xunfei decoder资源时出错: {e}") # 清理所有连接的音频缓冲区 if hasattr(self, "_connections"): diff --git a/main/xiaozhi-server/core/providers/vad/silero.py b/main/xiaozhi-server/core/providers/vad/silero.py index 738c2646..6fc26291 100644 --- a/main/xiaozhi-server/core/providers/vad/silero.py +++ b/main/xiaozhi-server/core/providers/vad/silero.py @@ -38,9 +38,12 @@ class VADProvider(VADProviderBase): self.frame_window_threshold = 3 def __del__(self): - if hasattr(self, 'decoder'): - del self.decoder - gc.collect() + if hasattr(self, 'decoder') and self.decoder is not None: + try: + del self.decoder + gc.collect() + except Exception: + pass def is_vad(self, conn, opus_packet): try: diff --git a/main/xiaozhi-server/core/utils/util.py b/main/xiaozhi-server/core/utils/util.py index 190c4d6a..9586ace8 100644 --- a/main/xiaozhi-server/core/utils/util.py +++ b/main/xiaozhi-server/core/utils/util.py @@ -395,8 +395,12 @@ def opus_datas_to_wav_bytes(opus_datas, sample_rate=16000, channels=1): wf.writeframes(pcm_bytes) return wav_buffer.getvalue() finally: - del decoder - gc.collect() + if decoder is not None: + try: + del decoder + gc.collect() + except Exception: + pass def check_vad_update(before_config, new_config):