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):