Merge branch 'main' into py_test_Memory_powermem

This commit is contained in:
wengzh
2026-02-05 17:45:07 +08:00
committed by GitHub
103 changed files with 21870 additions and 316 deletions
@@ -13,6 +13,10 @@ from datetime import datetime
from config.logger import setup_logging
from core.providers.asr.base import ASRProviderBase
from core.providers.asr.dto.dto import InterfaceType
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -146,7 +150,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).warning(f"发送音频失败: {str(e)}")
await self._cleanup()
async def _start_recognition(self, conn):
async def _start_recognition(self, conn: "ConnectionHandler"):
"""开始识别会话"""
if self._is_token_expired():
self._refresh_token()
@@ -192,7 +196,7 @@ class ASRProvider(ASRProviderBase):
await self.asr_ws.send(json.dumps(start_request, ensure_ascii=False))
logger.bind(tag=TAG).debug("已发送开始请求,等待服务器准备...")
async def _forward_results(self, conn):
async def _forward_results(self, conn: "ConnectionHandler"):
"""转发识别结果"""
try:
while not conn.stop_event.is_set():
@@ -3,7 +3,11 @@ import uuid
import asyncio
import websockets
import opuslib_next
from typing import List
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
@@ -74,7 +78,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).warning(f"发送音频失败: {str(e)}")
await self._cleanup()
async def _start_recognition(self, conn):
async def _start_recognition(self, conn: "ConnectionHandler"):
"""开始识别会话"""
try:
# 如果为手动模式,设置超时时长为最大值
@@ -154,7 +158,7 @@ class ASRProvider(ASRProviderBase):
return message
async def _forward_results(self, conn):
async def _forward_results(self, conn: "ConnectionHandler"):
"""转发识别结果"""
try:
while not conn.stop_event.is_set():
+11 -7
View File
@@ -5,21 +5,25 @@ import uuid
import json
import time
import queue
import shutil
import asyncio
import tempfile
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, NamedTuple
from core.providers.asr.dto.dto import InterfaceType
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
from typing import Optional, Tuple, List, NamedTuple, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -30,14 +34,14 @@ class ASRProviderBase(ABC):
pass
# 打开音频通道
async def open_audio_channels(self, conn):
async def open_audio_channels(self, conn: "ConnectionHandler"):
conn.asr_priority_thread = threading.Thread(
target=self.asr_text_priority_thread, args=(conn,), daemon=True
)
conn.asr_priority_thread.start()
# 有序处理ASR音频
def asr_text_priority_thread(self, conn):
def asr_text_priority_thread(self, conn: "ConnectionHandler"):
while not conn.stop_event.is_set():
try:
message = conn.asr_audio_queue.get(timeout=1)
@@ -55,7 +59,7 @@ class ASRProviderBase(ABC):
continue
# 接收音频
async def receive_audio(self, conn, audio, audio_have_voice):
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
if conn.client_listen_mode == "manual":
# 手动模式:缓存音频用于ASR识别
conn.asr_audio.append(audio)
@@ -77,7 +81,7 @@ class ASRProviderBase(ABC):
await self.handle_voice_stop(conn, asr_audio_task)
# 处理语音停止
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
async def handle_voice_stop(self, conn: "ConnectionHandler", asr_audio_task: List[bytes]):
"""并行处理ASR和声纹识别"""
try:
total_start_time = time.monotonic()
@@ -7,6 +7,10 @@ import opuslib_next
from core.providers.asr.base import ASRProviderBase
from config.logger import setup_logging
from core.providers.asr.dto.dto import InterfaceType
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -34,7 +38,9 @@ class ASRProvider(ASRProviderBase):
# 火山引擎ASR配置
enable_multilingual = config.get("enable_multilingual", False)
self.enable_multilingual = False if str(enable_multilingual).lower() == 'false' else True
self.enable_multilingual = (
False if str(enable_multilingual).lower() == "false" else True
)
if self.enable_multilingual:
self.ws_url = "wss://openspeech.bytedance.com/api/v3/sauc/bigmodel_nostream"
else:
@@ -59,10 +65,10 @@ class ASRProvider(ASRProviderBase):
async def open_audio_channels(self, conn):
await super().open_audio_channels(conn)
async def receive_audio(self, conn, audio, audio_have_voice):
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
# 先调用父类方法处理基础逻辑
await super().receive_audio(conn, audio, audio_have_voice)
# 如果本次有声音,且之前没有建立连接
if audio_have_voice and self.asr_ws is None and not self.is_processing:
try:
@@ -151,7 +157,7 @@ class ASRProvider(ASRProviderBase):
except Exception as e:
logger.bind(tag=TAG).info(f"发送音频数据时发生错误: {e}")
async def _forward_asr_results(self, conn):
async def _forward_asr_results(self, conn: "ConnectionHandler"):
try:
while self.asr_ws and not conn.stop_event.is_set():
# 获取当前连接的音频数据
@@ -172,8 +178,9 @@ class ASRProvider(ASRProviderBase):
utterances = payload["result"].get("utterances", [])
# 检查duration和空文本的情况
if (
not self.enable_multilingual # 注意:多语种模式不返回中间结果,需要等待最终结果
and payload.get("audio_info", {}).get("duration", 0) > 2000
not self.enable_multilingual # 注意:多语种模式不返回中间结果,需要等待最终结果
and payload.get("audio_info", {}).get("duration", 0)
> 2000
and not utterances
and not payload["result"].get("text")
and conn.client_listen_mode != "manual"
@@ -218,7 +225,9 @@ class ASRProvider(ASRProviderBase):
# 自动模式下直接覆盖
self.text = current_text
if len(audio_data) > 15: # 确保有足够音频数据
await self.handle_voice_stop(conn, audio_data)
await self.handle_voice_stop(
conn, audio_data
)
break
elif "error" in payload:
error_msg = payload.get("error", "未知错误")
@@ -260,7 +269,9 @@ class ASRProvider(ASRProviderBase):
try:
# 发送结束标记的音频帧(gzip压缩的空数据)
empty_payload = gzip.compress(b"")
last_audio_request = bytearray(self.generate_last_audio_default_header())
last_audio_request = bytearray(
self.generate_last_audio_default_header()
)
last_audio_request.extend(len(empty_payload).to_bytes(4, "big"))
last_audio_request.extend(empty_payload)
await self.asr_ws.send(last_audio_request)
@@ -406,9 +417,9 @@ class ASRProvider(ASRProviderBase):
pass
self.forward_task = None
self.is_processing = False
# 显式释放decoder资源
if hasattr(self, 'decoder') and self.decoder is not None:
if hasattr(self, "decoder") and self.decoder is not None:
try:
del self.decoder
self.decoder = None
@@ -9,7 +9,10 @@ import gc
from time import mktime
from datetime import datetime
from urllib.parse import urlencode
from typing import List
from typing import List, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from config.logger import setup_logging
from wsgiref.handlers import format_date_time
from core.providers.asr.base import ASRProviderBase
@@ -94,10 +97,10 @@ class ASRProvider(ASRProviderBase):
url = url + "?" + urlencode(v)
return url
async def open_audio_channels(self, conn):
async def open_audio_channels(self, conn: "ConnectionHandler"):
await super().open_audio_channels(conn)
async def receive_audio(self, conn, audio, audio_have_voice):
async def receive_audio(self, conn: "ConnectionHandler", audio, audio_have_voice):
# 先调用父类方法处理基础逻辑
await super().receive_audio(conn, audio, audio_have_voice)
@@ -119,7 +122,7 @@ class ASRProvider(ASRProviderBase):
logger.bind(tag=TAG).warning(f"发送音频数据时发生错误: {e}")
await self._cleanup()
async def _start_recognition(self, conn):
async def _start_recognition(self, conn: "ConnectionHandler"):
"""开始识别会话"""
try:
self.is_processing = True
@@ -189,7 +192,7 @@ class ASRProvider(ASRProviderBase):
await self.asr_ws.send(json.dumps(frame_data, ensure_ascii=False))
async def _forward_results(self, conn):
async def _forward_results(self, conn: "ConnectionHandler"):
"""转发识别结果"""
try:
while not conn.stop_event.is_set():
@@ -253,7 +256,9 @@ class ASRProvider(ASRProviderBase):
await self._cleanup()
conn.reset_audio_states()
async def handle_voice_stop(self, conn, asr_audio_task: List[bytes]):
async def handle_voice_stop(
self, conn: "ConnectionHandler", asr_audio_task: List[bytes]
):
"""处理语音停止,发送最后一帧并处理识别结果"""
try:
# 先发送最后一帧表示音频结束
@@ -338,7 +343,7 @@ class ASRProvider(ASRProviderBase):
self.is_processing = False
# 显式释放decoder资源
if hasattr(self, 'decoder') and self.decoder is not None:
if hasattr(self, "decoder") and self.decoder is not None:
try:
del self.decoder
self.decoder = None
@@ -1,4 +1,7 @@
from typing import List, Dict
from typing import List, Dict, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import IntentProviderBase
from plugins_func.functions.play_music import initialize_music_handler
from config.logger import setup_logging
@@ -129,7 +132,9 @@ class IntentProvider(IntentProviderBase):
logger.bind(tag=TAG).error(f"Error in generating reply result: {e}")
return get_system_error_response(self.config)
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
async def detect_intent(
self, conn: "ConnectionHandler", dialogue_history: List[Dict], text: str
) -> str:
if not self.llm:
raise ValueError("LLM provider not set")
if conn.func_handler is None:
@@ -3,12 +3,16 @@
import asyncio
from config.logger import setup_logging
from .iot_descriptor import IotDescriptor
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
async def handleIotDescriptors(conn, descriptors):
async def handleIotDescriptors(conn: "ConnectionHandler", descriptors):
"""处理物联网描述"""
wait_max_time = 5
while (
@@ -61,7 +65,7 @@ async def handleIotDescriptors(conn, descriptors):
conn.func_handler.current_support_functions()
async def handleIotStatus(conn, states):
async def handleIotStatus(conn: "ConnectionHandler", states):
"""处理物联网状态"""
for state in states:
for key, value in conn.iot_descriptors.items():
@@ -1,6 +1,9 @@
"""设备端MCP工具执行器"""
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import ToolType, ToolDefinition, ToolExecutor
from plugins_func.register import Action, ActionResponse
from .mcp_handler import call_mcp_tool
@@ -13,7 +16,7 @@ class DeviceMCPExecutor(ToolExecutor):
self.conn = conn
async def execute(
self, conn, tool_name: str, arguments: Dict[str, Any]
self, conn: "ConnectionHandler", tool_name: str, arguments: Dict[str, Any]
) -> ActionResponse:
"""执行设备端MCP工具"""
if not hasattr(conn, "mcp_client") or not conn.mcp_client:
@@ -7,6 +7,10 @@ from concurrent.futures import Future
from core.utils.util import get_vision_url, sanitize_tool_name
from core.utils.auth import AuthToken
from config.logger import setup_logging
from typing import TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
TAG = __name__
logger = setup_logging()
@@ -96,7 +100,7 @@ class MCPClient:
self.call_results.pop(id)
async def send_mcp_message(conn, payload: dict):
async def send_mcp_message(conn: "ConnectionHandler", payload: dict):
"""Helper to send MCP messages, encapsulating common logic."""
if not conn.features.get("mcp"):
logger.bind(tag=TAG).warning("客户端不支持MCP,无法发送MCP消息")
@@ -111,7 +115,9 @@ async def send_mcp_message(conn, payload: dict):
logger.bind(tag=TAG).error(f"发送MCP消息失败: {e}")
async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
async def handle_mcp_message(
conn: "ConnectionHandler", mcp_client: MCPClient, payload: dict
):
"""处理MCP消息,包括初始化、工具列表和工具调用响应等"""
logger.bind(tag=TAG).debug(f"处理MCP消息: {str(payload)[:100]}")
@@ -229,7 +235,7 @@ async def handle_mcp_message(conn, mcp_client: MCPClient, payload: dict):
)
async def send_mcp_initialize_message(conn):
async def send_mcp_initialize_message(conn: "ConnectionHandler"):
"""发送MCP初始化消息"""
vision_url = get_vision_url(conn.config)
@@ -264,7 +270,7 @@ async def send_mcp_initialize_message(conn):
await send_mcp_message(conn, payload)
async def send_mcp_tools_list_request(conn):
async def send_mcp_tools_list_request(conn: "ConnectionHandler"):
"""发送MCP工具列表请求"""
payload = {
"jsonrpc": "2.0",
@@ -275,7 +281,7 @@ async def send_mcp_tools_list_request(conn):
await send_mcp_message(conn, payload)
async def send_mcp_tools_list_continue_request(conn, cursor: str):
async def send_mcp_tools_list_continue_request(conn: "ConnectionHandler", cursor: str):
"""发送带有cursor的MCP工具列表请求"""
payload = {
"jsonrpc": "2.0",
@@ -288,7 +294,11 @@ async def send_mcp_tools_list_continue_request(conn, cursor: str):
async def call_mcp_tool(
conn, mcp_client: MCPClient, tool_name: str, args: str = "{}", timeout: int = 30
conn: "ConnectionHandler",
mcp_client: MCPClient,
tool_name: str,
args: str = "{}",
timeout: int = 30,
):
"""
调用指定的工具,并等待响应
@@ -1,6 +1,9 @@
"""服务端插件工具执行器"""
from typing import Dict, Any
from typing import Dict, Any, TYPE_CHECKING
if TYPE_CHECKING:
from core.connection import ConnectionHandler
from ..base import ToolType, ToolDefinition, ToolExecutor
from plugins_func.register import all_function_registry, Action, ActionResponse
@@ -8,12 +11,12 @@ from plugins_func.register import all_function_registry, Action, ActionResponse
class ServerPluginExecutor(ToolExecutor):
"""服务端插件工具执行器"""
def __init__(self, conn):
def __init__(self, conn: "ConnectionHandler"):
self.conn = conn
self.config = conn.config
async def execute(
self, conn, tool_name: str, arguments: Dict[str, Any]
self, conn: "ConnectionHandler", tool_name: str, arguments: Dict[str, Any]
) -> ActionResponse:
"""执行服务端插件工具"""
func_item = all_function_registry.get(tool_name)