mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 09:33:55 +08:00
update:server连接api (#747)
* update:server连接manager-api * update:读取智能体模型配置 * update:添加默认模型的按钮 * update:优化配置读取方式 * update:server兼容manager接口改造 * update:优化私有配置加载 * update:加载私有模型配置
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import copy
|
||||
import json
|
||||
import uuid
|
||||
import time
|
||||
@@ -17,16 +18,16 @@ from core.utils.util import (
|
||||
get_string_no_punctuation_or_emoji,
|
||||
extract_json_from_string,
|
||||
get_ip_info,
|
||||
initialize_modules,
|
||||
)
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||
from core.handle.sendAudioHandle import sendAudioMessage
|
||||
from core.handle.receiveAudioHandle import handleAudioMessage
|
||||
from core.handle.functionHandler import FunctionHandler
|
||||
from plugins_func.register import Action, ActionResponse
|
||||
from config.private_config import PrivateConfig
|
||||
from core.auth import AuthMiddleware, AuthenticationError
|
||||
from core.utils.auth_code_gen import AuthCodeGenerator
|
||||
from core.mcp.manager import MCPManager
|
||||
from config.config_loader import get_private_config_from_api
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -41,7 +42,7 @@ class ConnectionHandler:
|
||||
def __init__(
|
||||
self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent
|
||||
):
|
||||
self.config = config
|
||||
self.config = copy.deepcopy(config)
|
||||
self.logger = setup_logging()
|
||||
self.auth = AuthMiddleware(config)
|
||||
|
||||
@@ -95,15 +96,12 @@ class ConnectionHandler:
|
||||
self.iot_descriptors = {}
|
||||
self.func_handler = None
|
||||
|
||||
self.cmd_exit = self.config["CMD_exit"]
|
||||
self.cmd_exit = self.config["exit_commands"]
|
||||
self.max_cmd_length = 0
|
||||
for cmd in self.cmd_exit:
|
||||
if len(cmd) > self.max_cmd_length:
|
||||
self.max_cmd_length = len(cmd)
|
||||
|
||||
self.private_config = None
|
||||
self.auth_code_gen = AuthCodeGenerator.get_instance()
|
||||
self.is_device_verified = False # 添加设备验证状态标志
|
||||
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
||||
self.use_function_call_mode = False
|
||||
if self.config["selected_module"]["Intent"] == "function_call":
|
||||
@@ -121,7 +119,6 @@ class ConnectionHandler:
|
||||
|
||||
# 进行认证
|
||||
await self.auth.authenticate(self.headers)
|
||||
device_id = self.headers.get("device-id", None)
|
||||
|
||||
# 认证通过,继续处理
|
||||
self.websocket = ws
|
||||
@@ -130,39 +127,6 @@ class ConnectionHandler:
|
||||
self.welcome_msg = self.config["xiaozhi"]
|
||||
self.welcome_msg["session_id"] = self.session_id
|
||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
||||
# Load private configuration if device_id is provided
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
if bUsePrivateConfig and device_id:
|
||||
try:
|
||||
self.private_config = PrivateConfig(
|
||||
device_id, self.config, self.auth_code_gen
|
||||
)
|
||||
await self.private_config.load_or_create()
|
||||
# 判断是否已经绑定
|
||||
owner = self.private_config.get_owner()
|
||||
self.is_device_verified = owner is not None
|
||||
|
||||
if self.is_device_verified:
|
||||
await self.private_config.update_last_chat_time()
|
||||
|
||||
llm, tts = self.private_config.create_private_instances()
|
||||
if all([llm, tts]):
|
||||
self.llm = llm
|
||||
self.tts = tts
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"Loaded private config and instances for device {device_id}"
|
||||
)
|
||||
else:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"Failed to create instances for device {device_id}"
|
||||
)
|
||||
self.private_config = None
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"Error initializing private config: {e}"
|
||||
)
|
||||
self.private_config = None
|
||||
raise
|
||||
|
||||
# 异步初始化
|
||||
self.executor.submit(self._initialize_components)
|
||||
@@ -211,10 +175,11 @@ class ConnectionHandler:
|
||||
await handleAudioMessage(self, message)
|
||||
|
||||
def _initialize_components(self):
|
||||
"""初始化组件"""
|
||||
self._initialize_models()
|
||||
|
||||
"""加载提示词"""
|
||||
self.prompt = self.config["prompt"]
|
||||
if self.private_config:
|
||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||
|
||||
"""加载记忆"""
|
||||
@@ -222,13 +187,100 @@ class ConnectionHandler:
|
||||
"""加载意图识别"""
|
||||
self._initialize_intent()
|
||||
"""加载位置信息"""
|
||||
self.client_ip_info = get_ip_info(self.client_ip)
|
||||
self.client_ip_info = get_ip_info(self.client_ip, self.logger)
|
||||
if self.client_ip_info is not None and "city" in self.client_ip_info:
|
||||
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
|
||||
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
|
||||
|
||||
self.dialogue.update_system_message(self.prompt)
|
||||
|
||||
def _initialize_models(self):
|
||||
read_config_from_api = self.config.get("read_config_from_api", False)
|
||||
"""如果是从配置文件获取,则进行二次实例化"""
|
||||
if not read_config_from_api:
|
||||
return
|
||||
"""从接口获取差异化的配置进行二次实例化,非全量重新实例化"""
|
||||
try:
|
||||
private_config = get_private_config_from_api(
|
||||
self.config,
|
||||
self.headers.get("device-id", None),
|
||||
self.headers.get("client-id", None),
|
||||
)
|
||||
private_config["delete_audio"] = self.config["delete_audio"]
|
||||
self.logger.bind(tag=TAG).info(f"获取差异化配置成功: {private_config}")
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
|
||||
private_config = {}
|
||||
|
||||
init_vad, init_asr, init_llm, init_tts, init_memory, init_intent = (
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
if private_config.get("VAD", None) is not None:
|
||||
init_vad = True
|
||||
self.config["vad"] = private_config["VAD"]
|
||||
self.config["selected_module"]["VAD"] = private_config["selected_module"][
|
||||
"VAD"
|
||||
]
|
||||
if private_config.get("ASR", None) is not None:
|
||||
init_asr = True
|
||||
self.config["asr"] = private_config["ASR"]
|
||||
self.config["selected_module"]["ASR"] = private_config["selected_module"][
|
||||
"ASR"
|
||||
]
|
||||
if private_config.get("LLM", None) is not None:
|
||||
init_llm = True
|
||||
self.config["llm"] = private_config["LLM"]
|
||||
self.config["selected_module"]["LLM"] = private_config["selected_module"][
|
||||
"LLM"
|
||||
]
|
||||
if private_config.get("TTS", None) is not None:
|
||||
init_tts = True
|
||||
self.config["tts"] = private_config["TTS"]
|
||||
self.config["selected_module"]["TTS"] = private_config["selected_module"][
|
||||
"TTS"
|
||||
]
|
||||
if private_config.get("Memory", None) is not None:
|
||||
init_memory = True
|
||||
self.config["memory"] = private_config["Memory"]
|
||||
self.config["selected_module"]["Memory"] = private_config[
|
||||
"selected_module"
|
||||
]["Memory"]
|
||||
if private_config.get("Intent", None) is not None:
|
||||
init_intent = True
|
||||
self.config["intent"] = private_config["Intent"]
|
||||
self.config["selected_module"]["Intent"] = private_config[
|
||||
"selected_module"
|
||||
]["Intent"]
|
||||
if private_config.get("prompt", None) is not None:
|
||||
self.config["prompt"] = private_config["prompt"]
|
||||
try:
|
||||
modules = initialize_modules(
|
||||
self.logger,
|
||||
private_config,
|
||||
init_vad,
|
||||
init_asr,
|
||||
init_llm,
|
||||
init_tts,
|
||||
init_memory,
|
||||
init_intent,
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"初始化组件失败: {e}")
|
||||
modules = {}
|
||||
if modules.get("tts", None) is not None:
|
||||
self.tts = modules["tts"]
|
||||
if modules.get("llm", None) is not None:
|
||||
self.llm = modules["llm"]
|
||||
if modules.get("intent", None) is not None:
|
||||
self.intent = modules["intent"]
|
||||
if modules.get("memory", None) is not None:
|
||||
self.memory = modules["memory"]
|
||||
|
||||
def _initialize_memory(self):
|
||||
"""初始化记忆模块"""
|
||||
device_id = self.headers.get("device-id", None)
|
||||
@@ -281,34 +333,7 @@ class ConnectionHandler:
|
||||
if m.role == "system":
|
||||
m.content = prompt
|
||||
|
||||
async def _check_and_broadcast_auth_code(self):
|
||||
"""检查设备绑定状态并广播认证码"""
|
||||
if not self.private_config.get_owner():
|
||||
auth_code = self.private_config.get_auth_code()
|
||||
if auth_code:
|
||||
# 发送验证码语音提示
|
||||
text = f"请在后台输入验证码:{' '.join(auth_code)}"
|
||||
self.recode_first_last_text(text)
|
||||
future = self.executor.submit(self.speak_and_play, text)
|
||||
self.tts_queue.put(future)
|
||||
return False
|
||||
return True
|
||||
|
||||
def isNeedAuth(self):
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
if not bUsePrivateConfig:
|
||||
# 如果不使用私有配置,就不需要验证
|
||||
return False
|
||||
return not self.is_device_verified
|
||||
|
||||
def chat(self, query):
|
||||
if self.isNeedAuth():
|
||||
self.llm_finish_task = True
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self._check_and_broadcast_auth_code(), self.loop
|
||||
)
|
||||
future.result()
|
||||
return True
|
||||
|
||||
self.dialogue.put(Message(role="user", content=query))
|
||||
|
||||
@@ -391,13 +416,6 @@ class ConnectionHandler:
|
||||
def chat_with_function_calling(self, query, tool_call=False):
|
||||
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
|
||||
"""Chat with function calling for intent detection using streaming"""
|
||||
if self.isNeedAuth():
|
||||
self.llm_finish_task = True
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self._check_and_broadcast_auth_code(), self.loop
|
||||
)
|
||||
future.result()
|
||||
return True
|
||||
|
||||
if not tool_call:
|
||||
self.dialogue.put(Message(role="user", content=query))
|
||||
|
||||
@@ -8,6 +8,7 @@ import os
|
||||
import random
|
||||
import time
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
WAKEUP_CONFIG = {
|
||||
@@ -69,6 +70,14 @@ def getWakeupWordFile(file_name):
|
||||
|
||||
|
||||
async def wakeupWordsResponse(conn):
|
||||
wait_max_time = 5
|
||||
while conn.llm is None or not conn.llm.response_no_stream:
|
||||
await asyncio.sleep(1)
|
||||
wait_max_time -= 1
|
||||
if wait_max_time <= 0:
|
||||
logger.bind(tag=TAG).error("连接对象没有llm")
|
||||
return
|
||||
|
||||
"""唤醒词响应"""
|
||||
wakeup_word = random.choice(WAKEUP_CONFIG["words"])
|
||||
result = conn.llm.response_no_stream(conn.config["prompt"], wakeup_word)
|
||||
|
||||
@@ -1,26 +1,30 @@
|
||||
"""MCP服务管理器"""
|
||||
|
||||
import os, json
|
||||
from typing import Dict, Any, List
|
||||
from .MCPClient import MCPClient
|
||||
from config.logger import setup_logging
|
||||
from core.utils.util import get_project_dir
|
||||
from plugins_func.register import register_function, ActionResponse, Action, ToolType
|
||||
from plugins_func.register import register_function, ToolType
|
||||
from config.config_loader import get_project_dir
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class MCPManager:
|
||||
"""管理多个MCP服务的集中管理器"""
|
||||
|
||||
def __init__(self,conn) -> None:
|
||||
def __init__(self, conn) -> None:
|
||||
"""
|
||||
初始化MCP管理器
|
||||
"""
|
||||
self.conn = conn
|
||||
self.logger = setup_logging()
|
||||
self.config_path = get_project_dir() + 'data/.mcp_server_settings.json'
|
||||
self.config_path = get_project_dir() + "data/.mcp_server_settings.json"
|
||||
if os.path.exists(self.config_path) == False:
|
||||
self.config_path = ""
|
||||
self.logger.bind(tag=TAG).warning(f"请检查mcp服务配置文件:data/.mcp_server_settings.json")
|
||||
self.logger.bind(tag=TAG).warning(
|
||||
f"请检查mcp服务配置文件:data/.mcp_server_settings.json"
|
||||
)
|
||||
self.client: Dict[str, MCPClient] = {}
|
||||
self.tools = []
|
||||
|
||||
@@ -31,13 +35,15 @@ class MCPManager:
|
||||
"""
|
||||
if len(self.config_path) == 0:
|
||||
return {}
|
||||
|
||||
|
||||
try:
|
||||
with open(self.config_path, 'r', encoding='utf-8') as f:
|
||||
with open(self.config_path, "r", encoding="utf-8") as f:
|
||||
config = json.load(f)
|
||||
return config.get('mcpServers', {})
|
||||
return config.get("mcpServers", {})
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"Error loading MCP config from {self.config_path}: {e}")
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"Error loading MCP config from {self.config_path}: {e}"
|
||||
)
|
||||
return {}
|
||||
|
||||
async def initialize_servers(self) -> None:
|
||||
@@ -45,7 +51,9 @@ class MCPManager:
|
||||
config = self.load_config()
|
||||
for name, srv_config in config.items():
|
||||
if not srv_config.get("command"):
|
||||
self.logger.bind(tag=TAG).warning(f"Skipping server {name}: command not specified")
|
||||
self.logger.bind(tag=TAG).warning(
|
||||
f"Skipping server {name}: command not specified"
|
||||
)
|
||||
continue
|
||||
|
||||
try:
|
||||
@@ -56,12 +64,18 @@ class MCPManager:
|
||||
client_tools = client.get_available_tools()
|
||||
self.tools.extend(client_tools)
|
||||
for tool in client_tools:
|
||||
func_name = "mcp_"+tool["function"]["name"]
|
||||
register_function(func_name, tool, ToolType.MCP_CLIENT)(self.execute_tool)
|
||||
self.conn.func_handler.function_registry.register_function(func_name)
|
||||
func_name = "mcp_" + tool["function"]["name"]
|
||||
register_function(func_name, tool, ToolType.MCP_CLIENT)(
|
||||
self.execute_tool
|
||||
)
|
||||
self.conn.func_handler.function_registry.register_function(
|
||||
func_name
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"Failed to initialize MCP server {name}: {e}")
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"Failed to initialize MCP server {name}: {e}"
|
||||
)
|
||||
self.conn.func_handler.upload_functions_desc()
|
||||
|
||||
def get_all_tools(self) -> List[Dict[str, Any]]:
|
||||
@@ -79,7 +93,10 @@ class MCPManager:
|
||||
bool: 是否是MCP工具
|
||||
"""
|
||||
for tool in self.tools:
|
||||
if tool.get("function") != None and tool["function"].get("name") == tool_name:
|
||||
if (
|
||||
tool.get("function") != None
|
||||
and tool["function"].get("name") == tool_name
|
||||
):
|
||||
return True
|
||||
return False
|
||||
|
||||
@@ -87,17 +104,19 @@ class MCPManager:
|
||||
"""执行工具调用
|
||||
Args:
|
||||
tool_name: 工具名称
|
||||
arguments: 工具参数
|
||||
arguments: 工具参数
|
||||
Returns:
|
||||
Any: 工具执行结果
|
||||
Raises:
|
||||
ValueError: 工具未找到时抛出
|
||||
"""
|
||||
self.logger.bind(tag=TAG).info(f"Executing tool {tool_name} with arguments: {arguments}")
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"Executing tool {tool_name} with arguments: {arguments}"
|
||||
)
|
||||
for client in self.client.values():
|
||||
if client.has_tool(tool_name):
|
||||
return await client.call_tool(tool_name, arguments)
|
||||
|
||||
|
||||
raise ValueError(f"Tool {tool_name} not found in any MCP server")
|
||||
|
||||
async def cleanup_all(self) -> None:
|
||||
@@ -106,5 +125,7 @@ class MCPManager:
|
||||
await client.cleanup()
|
||||
self.logger.bind(tag=TAG).info(f"Cleaned up MCP client: {name}")
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"Error cleaning up MCP client {name}: {e}")
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"Error cleaning up MCP client {name}: {e}"
|
||||
)
|
||||
self.client.clear()
|
||||
|
||||
@@ -4,15 +4,17 @@ from core.providers.llm.base import LLMProviderBase
|
||||
from config.logger import setup_logging
|
||||
import requests
|
||||
import json
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
"""初始化Gemini LLM Provider"""
|
||||
self.model_name = config.get("model_name", "gemini-1.5-pro")
|
||||
self.api_key = config.get("api_key")
|
||||
self.http_proxy=config.get("http_proxy")
|
||||
self.http_proxy = config.get("http_proxy")
|
||||
self.https_proxy = config.get("https_proxy")
|
||||
have_key = check_model_key("LLM", self.api_key)
|
||||
|
||||
@@ -22,7 +24,7 @@ class LLMProvider(LLMProviderBase):
|
||||
try:
|
||||
# 初始化Gemini客户端
|
||||
# 配置代理(如果提供了代理配置)
|
||||
self.proxies=None
|
||||
self.proxies = None
|
||||
if self.http_proxy is not "" or self.https_proxy is not "":
|
||||
|
||||
self.proxies = {
|
||||
@@ -62,19 +64,16 @@ class LLMProvider(LLMProviderBase):
|
||||
role = "model" if msg["role"] == "assistant" else "user"
|
||||
content = msg["content"].strip()
|
||||
if content:
|
||||
chat_history.append({
|
||||
"role": role,
|
||||
"parts": [{"text":content}]
|
||||
|
||||
})
|
||||
chat_history.append({"role": role, "parts": [{"text": content}]})
|
||||
|
||||
# 获取当前消息
|
||||
current_msg = dialogue[-1]["content"]
|
||||
|
||||
# 构建请求体
|
||||
request_body = {
|
||||
"contents": chat_history + [{"role": "user", "parts": [{"text":current_msg}]}],
|
||||
"generationConfig": self.generation_config
|
||||
"contents": chat_history
|
||||
+ [{"role": "user", "parts": [{"text": current_msg}]}],
|
||||
"generationConfig": self.generation_config,
|
||||
}
|
||||
|
||||
# 构建请求URL
|
||||
@@ -87,11 +86,17 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
# 发送POST请求,经测试手动 request 无法使用 stream 模式
|
||||
if self.proxies:
|
||||
response = requests.post(url, headers=headers, json=request_body, stream=False, proxies=self.proxies)
|
||||
response = requests.post(
|
||||
url,
|
||||
headers=headers,
|
||||
json=request_body,
|
||||
stream=False,
|
||||
proxies=self.proxies,
|
||||
)
|
||||
try:
|
||||
data = response.json() # 直接解析JSON
|
||||
if 'candidates' in data and data['candidates']:
|
||||
yield data['candidates'][0]['content']['parts'][0]['text']
|
||||
if "candidates" in data and data["candidates"]:
|
||||
yield data["candidates"][0]["content"]["parts"][0]["text"]
|
||||
else:
|
||||
yield "未找到候选回复。"
|
||||
except json.JSONDecodeError as e:
|
||||
@@ -104,13 +109,11 @@ class LLMProvider(LLMProviderBase):
|
||||
|
||||
# 发送消息并获取流式响应
|
||||
response = chat.send_message(
|
||||
current_msg,
|
||||
stream=True,
|
||||
generation_config=self.generation_config
|
||||
current_msg, stream=True, generation_config=self.generation_config
|
||||
)
|
||||
# 处理流式响应
|
||||
for chunk in response:
|
||||
if hasattr(chunk, 'text') and chunk.text:
|
||||
if hasattr(chunk, "text") and chunk.text:
|
||||
yield chunk.text
|
||||
|
||||
except Exception as e:
|
||||
@@ -125,9 +128,6 @@ class LLMProvider(LLMProviderBase):
|
||||
else:
|
||||
yield f"【Gemini服务响应异常: {error_msg}】"
|
||||
|
||||
|
||||
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
yield f"请求失败:{e}"
|
||||
except json.JSONDecodeError as e:
|
||||
|
||||
@@ -11,7 +11,7 @@ class LLMProvider(LLMProviderBase):
|
||||
def __init__(self, config):
|
||||
self.model_name = config.get("model_name")
|
||||
self.api_key = config.get("api_key")
|
||||
if 'base_url' in config:
|
||||
if "base_url" in config:
|
||||
self.base_url = config.get("base_url")
|
||||
else:
|
||||
self.base_url = config.get("url")
|
||||
@@ -33,18 +33,22 @@ class LLMProvider(LLMProviderBase):
|
||||
for chunk in responses:
|
||||
try:
|
||||
# 检查是否存在有效的choice且content不为空
|
||||
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||
content = delta.content if hasattr(delta, 'content') else ''
|
||||
delta = (
|
||||
chunk.choices[0].delta
|
||||
if getattr(chunk, "choices", None)
|
||||
else None
|
||||
)
|
||||
content = delta.content if hasattr(delta, "content") else ""
|
||||
except IndexError:
|
||||
content = ''
|
||||
content = ""
|
||||
if content:
|
||||
# 处理标签跨多个chunk的情况
|
||||
if '<think>' in content:
|
||||
if "<think>" in content:
|
||||
is_active = False
|
||||
content = content.split('<think>')[0]
|
||||
if '</think>' in content:
|
||||
content = content.split("<think>")[0]
|
||||
if "</think>" in content:
|
||||
is_active = True
|
||||
content = content.split('</think>')[-1]
|
||||
content = content.split("</think>")[-1]
|
||||
if is_active:
|
||||
yield content
|
||||
|
||||
@@ -54,10 +58,7 @@ class LLMProvider(LLMProviderBase):
|
||||
def response_with_functions(self, session_id, dialogue, functions=None):
|
||||
try:
|
||||
stream = self.client.chat.completions.create(
|
||||
model=self.model_name,
|
||||
messages=dialogue,
|
||||
stream=True,
|
||||
tools=functions
|
||||
model=self.model_name, messages=dialogue, stream=True, tools=functions
|
||||
)
|
||||
|
||||
for chunk in stream:
|
||||
|
||||
@@ -6,13 +6,14 @@ from core.utils.util import check_model_key
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class MemoryProvider(MemoryProviderBase):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.api_key = config.get("api_key", "")
|
||||
self.api_version = config.get("api_version", "v1.1")
|
||||
have_key = check_model_key("Mem0ai", self.api_key)
|
||||
if not have_key :
|
||||
if not have_key:
|
||||
self.use_mem0 = False
|
||||
return
|
||||
else:
|
||||
@@ -30,54 +31,55 @@ class MemoryProvider(MemoryProviderBase):
|
||||
return None
|
||||
if len(msgs) < 2:
|
||||
return None
|
||||
|
||||
|
||||
try:
|
||||
# Format the content as a message list for mem0
|
||||
messages = [
|
||||
{"role": message.role, "content": message.content}
|
||||
for message in msgs if message.role != "system"
|
||||
for message in msgs
|
||||
if message.role != "system"
|
||||
]
|
||||
result = self.client.add(messages, user_id=self.role_id, output_format=self.api_version)
|
||||
result = self.client.add(
|
||||
messages, user_id=self.role_id, output_format=self.api_version
|
||||
)
|
||||
logger.bind(tag=TAG).debug(f"Save memory result: {result}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"保存记忆失败: {str(e)}")
|
||||
return None
|
||||
|
||||
async def query_memory(self, query: str)-> str:
|
||||
async def query_memory(self, query: str) -> str:
|
||||
if not self.use_mem0:
|
||||
return ""
|
||||
try:
|
||||
results = self.client.search(
|
||||
query,
|
||||
user_id=self.role_id,
|
||||
output_format=self.api_version
|
||||
query, user_id=self.role_id, output_format=self.api_version
|
||||
)
|
||||
if not results or 'results' not in results:
|
||||
if not results or "results" not in results:
|
||||
return ""
|
||||
|
||||
|
||||
# Format each memory entry with its update time up to minutes
|
||||
memories = []
|
||||
for entry in results['results']:
|
||||
timestamp = entry.get('updated_at', '')
|
||||
for entry in results["results"]:
|
||||
timestamp = entry.get("updated_at", "")
|
||||
if timestamp:
|
||||
try:
|
||||
# Parse and reformat the timestamp
|
||||
dt = timestamp.split('.')[0] # Remove milliseconds
|
||||
formatted_time = dt.replace('T', ' ')
|
||||
dt = timestamp.split(".")[0] # Remove milliseconds
|
||||
formatted_time = dt.replace("T", " ")
|
||||
except:
|
||||
formatted_time = timestamp
|
||||
memory = entry.get('memory', '')
|
||||
memory = entry.get("memory", "")
|
||||
if timestamp and memory:
|
||||
# Store tuple of (timestamp, formatted_string) for sorting
|
||||
memories.append((timestamp, f"[{formatted_time}] {memory}"))
|
||||
|
||||
|
||||
# Sort by timestamp in descending order (newest first)
|
||||
memories.sort(key=lambda x: x[0], reverse=True)
|
||||
|
||||
|
||||
# Extract only the formatted strings
|
||||
memories_str = "\n".join(f"- {memory[1]}" for memory in memories)
|
||||
logger.bind(tag=TAG).debug(f"Query results: {memories_str}")
|
||||
return memories_str
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"查询记忆失败: {str(e)}")
|
||||
return ""
|
||||
return ""
|
||||
|
||||
@@ -3,7 +3,8 @@ import time
|
||||
import json
|
||||
import os
|
||||
import yaml
|
||||
from core.utils.util import get_project_dir
|
||||
from config.config_loader import get_project_dir
|
||||
|
||||
|
||||
short_term_memory_prompt = """
|
||||
# 时空记忆编织者
|
||||
@@ -71,11 +72,12 @@ short_term_memory_prompt = """
|
||||
```
|
||||
"""
|
||||
|
||||
|
||||
def extract_json_data(json_code):
|
||||
start = json_code.find("```json")
|
||||
# 从start开始找到下一个```结束
|
||||
end = json_code.find("```", start+1)
|
||||
#print("start:", start, "end:", end)
|
||||
end = json_code.find("```", start + 1)
|
||||
# print("start:", start, "end:", end)
|
||||
if start == -1 or end == -1:
|
||||
try:
|
||||
jsonData = json.loads(json_code)
|
||||
@@ -83,74 +85,76 @@ def extract_json_data(json_code):
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
return ""
|
||||
jsonData = json_code[start+7:end]
|
||||
jsonData = json_code[start + 7 : end]
|
||||
return jsonData
|
||||
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class MemoryProvider(MemoryProviderBase):
|
||||
def __init__(self, config):
|
||||
super().__init__(config)
|
||||
self.short_momery = ""
|
||||
self.memory_path = get_project_dir() + 'data/.memory.yaml'
|
||||
self.memory_path = get_project_dir() + "data/.memory.yaml"
|
||||
self.load_memory()
|
||||
|
||||
def init_memory(self, role_id, llm):
|
||||
super().init_memory(role_id, llm)
|
||||
self.load_memory()
|
||||
|
||||
|
||||
def load_memory(self):
|
||||
all_memory = {}
|
||||
if os.path.exists(self.memory_path):
|
||||
with open(self.memory_path, 'r', encoding='utf-8') as f:
|
||||
with open(self.memory_path, "r", encoding="utf-8") as f:
|
||||
all_memory = yaml.safe_load(f) or {}
|
||||
if self.role_id in all_memory:
|
||||
self.short_momery = all_memory[self.role_id]
|
||||
|
||||
|
||||
def save_memory_to_file(self):
|
||||
all_memory = {}
|
||||
if os.path.exists(self.memory_path):
|
||||
with open(self.memory_path, 'r', encoding='utf-8') as f:
|
||||
all_memory = yaml.safe_load(f) or {}
|
||||
with open(self.memory_path, "r", encoding="utf-8") as f:
|
||||
all_memory = yaml.safe_load(f) or {}
|
||||
all_memory[self.role_id] = self.short_momery
|
||||
with open(self.memory_path, 'w', encoding='utf-8') as f:
|
||||
with open(self.memory_path, "w", encoding="utf-8") as f:
|
||||
yaml.dump(all_memory, f, allow_unicode=True)
|
||||
|
||||
|
||||
async def save_memory(self, msgs):
|
||||
if self.llm is None:
|
||||
logger.bind(tag=TAG).error("LLM is not set for memory provider")
|
||||
return None
|
||||
|
||||
|
||||
if len(msgs) < 2:
|
||||
return None
|
||||
|
||||
|
||||
msgStr = ""
|
||||
for msg in msgs:
|
||||
if msg.role == "user":
|
||||
msgStr += f"User: {msg.content}\n"
|
||||
elif msg.role== "assistant":
|
||||
elif msg.role == "assistant":
|
||||
msgStr += f"Assistant: {msg.content}\n"
|
||||
if len(self.short_momery) > 0:
|
||||
msgStr+="历史记忆:\n"
|
||||
msgStr+=self.short_momery
|
||||
|
||||
#当前时间
|
||||
msgStr += "历史记忆:\n"
|
||||
msgStr += self.short_momery
|
||||
|
||||
# 当前时间
|
||||
time_str = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
|
||||
msgStr += f"当前时间:{time_str}"
|
||||
|
||||
result = self.llm.response_no_stream(short_term_memory_prompt, msgStr)
|
||||
|
||||
|
||||
json_str = extract_json_data(result)
|
||||
try:
|
||||
json_data = json.loads(json_str) # 检查json格式是否正确
|
||||
json_data = json.loads(json_str) # 检查json格式是否正确
|
||||
self.short_momery = json_str
|
||||
except Exception as e:
|
||||
print("Error:", e)
|
||||
|
||||
|
||||
self.save_memory_to_file()
|
||||
logger.bind(tag=TAG).info(f"Save memory successful - Role: {self.role_id}")
|
||||
|
||||
return self.short_momery
|
||||
|
||||
async def query_memory(self, query: str)-> str:
|
||||
return self.short_momery
|
||||
|
||||
async def query_memory(self, query: str) -> str:
|
||||
return self.short_momery
|
||||
|
||||
@@ -6,6 +6,10 @@ import requests
|
||||
from datetime import datetime
|
||||
from core.utils.util import check_model_key
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
@@ -21,18 +25,19 @@ class TTSProvider(TTSProviderBase):
|
||||
check_model_key("TTS", self.access_token)
|
||||
|
||||
def generate_filename(self, extension=".wav"):
|
||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||
return os.path.join(
|
||||
self.output_file,
|
||||
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||
)
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
request_json = {
|
||||
"app": {
|
||||
"appid": f"{self.appid}",
|
||||
"token": "access_token",
|
||||
"cluster": self.cluster
|
||||
},
|
||||
"user": {
|
||||
"uid": "1"
|
||||
"cluster": self.cluster,
|
||||
},
|
||||
"user": {"uid": "1"},
|
||||
"audio": {
|
||||
"voice_type": self.voice,
|
||||
"encoding": "wav",
|
||||
@@ -46,17 +51,21 @@ class TTSProvider(TTSProviderBase):
|
||||
"text_type": "plain",
|
||||
"operation": "query",
|
||||
"with_frontend": 1,
|
||||
"frontend_type": "unitTson"
|
||||
}
|
||||
"frontend_type": "unitTson",
|
||||
},
|
||||
}
|
||||
|
||||
try:
|
||||
resp = requests.post(self.api_url, json.dumps(request_json), headers=self.header)
|
||||
resp = requests.post(
|
||||
self.api_url, json.dumps(request_json), headers=self.header
|
||||
)
|
||||
if "data" in resp.json():
|
||||
data = resp.json()["data"]
|
||||
file_to_save = open(output_file, "wb")
|
||||
file_to_save.write(base64.b64decode(data))
|
||||
else:
|
||||
raise Exception(f"{__name__} status_code: {resp.status_code} response: {resp.content}")
|
||||
raise Exception(
|
||||
f"{__name__} status_code: {resp.status_code} response: {resp.content}"
|
||||
)
|
||||
except Exception as e:
|
||||
raise Exception(f"{__name__} error: {e}")
|
||||
|
||||
@@ -24,7 +24,7 @@ class ServeReferenceAudio(BaseModel):
|
||||
def decode_audio(cls, values):
|
||||
audio = values.get("audio")
|
||||
if (
|
||||
isinstance(audio, str) and len(audio) > 255
|
||||
isinstance(audio, str) and len(audio) > 255
|
||||
): # Check if audio is a string (Base64)
|
||||
try:
|
||||
values["audio"] = base64.b64decode(audio)
|
||||
@@ -107,7 +107,10 @@ class TTSProvider(TTSProviderBase):
|
||||
self.api_url = config.get("api_url", "http://127.0.0.1:8080/v1/tts")
|
||||
|
||||
def generate_filename(self, extension=".wav"):
|
||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||
return os.path.join(
|
||||
self.output_file,
|
||||
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||
)
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
# Prepare reference data
|
||||
@@ -117,9 +120,7 @@ class TTSProvider(TTSProviderBase):
|
||||
data = {
|
||||
"text": text,
|
||||
"references": [
|
||||
ServeReferenceAudio(
|
||||
audio=audio if audio else b"", text=text
|
||||
)
|
||||
ServeReferenceAudio(audio=audio if audio else b"", text=text)
|
||||
for text, audio in zip(ref_texts, byte_audios)
|
||||
],
|
||||
"reference_id": self.reference_id,
|
||||
@@ -139,7 +140,9 @@ class TTSProvider(TTSProviderBase):
|
||||
|
||||
response = requests.post(
|
||||
self.api_url,
|
||||
data=ormsgpack.packb(pydantic_data, option=ormsgpack.OPT_SERIALIZE_PYDANTIC),
|
||||
data=ormsgpack.packb(
|
||||
pydantic_data, option=ormsgpack.OPT_SERIALIZE_PYDANTIC
|
||||
),
|
||||
headers={
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/msgpack",
|
||||
@@ -152,8 +155,6 @@ class TTSProvider(TTSProviderBase):
|
||||
with open(output_file, "wb") as audio_file:
|
||||
audio_file.write(audio_content)
|
||||
|
||||
|
||||
|
||||
else:
|
||||
print(f"Request failed with status code {response.status_code}")
|
||||
print(response.json())
|
||||
|
||||
@@ -4,6 +4,11 @@ import requests
|
||||
from datetime import datetime
|
||||
from core.utils.util import check_model_key
|
||||
from core.providers.tts.base import TTSProviderBase
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class TTSProvider(TTSProviderBase):
|
||||
def __init__(self, config, delete_audio_file):
|
||||
@@ -18,23 +23,28 @@ class TTSProvider(TTSProviderBase):
|
||||
check_model_key("TTS", self.api_key)
|
||||
|
||||
def generate_filename(self, extension=".wav"):
|
||||
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
||||
return os.path.join(
|
||||
self.output_file,
|
||||
f"tts-{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||
)
|
||||
|
||||
async def text_to_speak(self, text, output_file):
|
||||
headers = {
|
||||
"Authorization": f"Bearer {self.api_key}",
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
data = {
|
||||
"model": self.model,
|
||||
"input": text,
|
||||
"voice": self.voice,
|
||||
"response_format": "wav",
|
||||
"speed": self.speed
|
||||
"speed": self.speed,
|
||||
}
|
||||
response = requests.post(self.api_url, json=data, headers=headers)
|
||||
if response.status_code == 200:
|
||||
with open(output_file, "wb") as audio_file:
|
||||
audio_file.write(response.content)
|
||||
else:
|
||||
raise Exception(f"OpenAI TTS请求失败: {response.status_code} - {response.text}")
|
||||
raise Exception(
|
||||
f"OpenAI TTS请求失败: {response.status_code} - {response.text}"
|
||||
)
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Optional
|
||||
|
||||
|
||||
class VADProviderBase(ABC):
|
||||
@abstractmethod
|
||||
def is_vad(self, conn, data) -> bool:
|
||||
"""检测音频数据中的语音活动"""
|
||||
pass
|
||||
@@ -0,0 +1,63 @@
|
||||
import time
|
||||
import numpy as np
|
||||
import torch
|
||||
import opuslib_next
|
||||
from config.logger import setup_logging
|
||||
from core.providers.vad.base import VADProviderBase
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class VADProvider(VADProviderBase):
|
||||
def __init__(self, config):
|
||||
logger.bind(tag=TAG).info("SileroVAD", config)
|
||||
self.model, self.utils = torch.hub.load(
|
||||
repo_or_dir=config["model_dir"],
|
||||
source="local",
|
||||
model="silero_vad",
|
||||
force_reload=False,
|
||||
)
|
||||
(get_speech_timestamps, _, _, _, _) = self.utils
|
||||
|
||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||
self.vad_threshold = config.get("threshold")
|
||||
self.silence_threshold_ms = config.get("min_silence_duration_ms")
|
||||
|
||||
def is_vad(self, conn, opus_packet):
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(opus_packet, 960)
|
||||
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
||||
|
||||
# 处理缓冲区中的完整帧(每次处理512采样点)
|
||||
client_have_voice = False
|
||||
while len(conn.client_audio_buffer) >= 512 * 2:
|
||||
# 提取前512个采样点(1024字节)
|
||||
chunk = conn.client_audio_buffer[: 512 * 2]
|
||||
conn.client_audio_buffer = conn.client_audio_buffer[512 * 2 :]
|
||||
|
||||
# 转换为模型需要的张量格式
|
||||
audio_int16 = np.frombuffer(chunk, dtype=np.int16)
|
||||
audio_float32 = audio_int16.astype(np.float32) / 32768.0
|
||||
audio_tensor = torch.from_numpy(audio_float32)
|
||||
|
||||
# 检测语音活动
|
||||
speech_prob = self.model(audio_tensor, 16000).item()
|
||||
client_have_voice = speech_prob >= self.vad_threshold
|
||||
|
||||
# 如果之前有声音,但本次没有声音,且与上次有声音的时间查已经超过了静默阈值,则认为已经说完一句话
|
||||
if conn.client_have_voice and not client_have_voice:
|
||||
stop_duration = (
|
||||
time.time() * 1000 - conn.client_have_voice_last_time
|
||||
)
|
||||
if stop_duration >= self.silence_threshold_ms:
|
||||
conn.client_voice_stop = True
|
||||
if client_have_voice:
|
||||
conn.client_have_voice = True
|
||||
conn.client_have_voice_last_time = time.time() * 1000
|
||||
|
||||
return client_have_voice
|
||||
except opuslib_next.OpusError as e:
|
||||
logger.bind(tag=TAG).info(f"解码错误: {e}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")
|
||||
@@ -1,97 +0,0 @@
|
||||
import random
|
||||
import threading
|
||||
import time
|
||||
from typing import Set
|
||||
|
||||
class AuthCodeGenerator:
|
||||
_instance = None
|
||||
_instance_lock = threading.Lock()
|
||||
|
||||
def __new__(cls):
|
||||
if not cls._instance:
|
||||
with cls._instance_lock:
|
||||
if not cls._instance:
|
||||
cls._instance = super(AuthCodeGenerator, cls).__new__(cls)
|
||||
# 初始化随机种子
|
||||
random.seed(time.time())
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
# 确保 __init__ 只被调用一次
|
||||
if not hasattr(self, '_initialized'):
|
||||
self._used_codes: Set[str] = set()
|
||||
self._code_timestamps = {}
|
||||
self._lock = threading.Lock()
|
||||
self._code_timeout = 3 * 24 * 60 * 60
|
||||
self._initialized = True
|
||||
|
||||
@classmethod
|
||||
def get_instance(cls):
|
||||
"""获取AuthCodeGenerator的单例实例"""
|
||||
return cls()
|
||||
|
||||
def generate_code(self) -> str:
|
||||
"""
|
||||
生成6位数字认证码,确保不重复
|
||||
返回: 6位数字字符串
|
||||
"""
|
||||
with self._lock:
|
||||
self._clean_expired_codes() # 清理过期code
|
||||
while True:
|
||||
# 使用时间戳和已用码数量作为种子,确保每次生成不同的随机数
|
||||
seed = int(time.time() * 1000) + len(self._used_codes)
|
||||
random.seed(seed)
|
||||
|
||||
# 生成6位随机数字
|
||||
code = ''.join(str(random.randint(0, 9)) for _ in range(6))
|
||||
|
||||
# 检查是否已存在
|
||||
if code not in self._used_codes:
|
||||
self._used_codes.add(code)
|
||||
self._code_timestamps[code] = time.time()
|
||||
return code
|
||||
|
||||
def remove_code(self, code: str) -> bool:
|
||||
"""
|
||||
删除已使用的认证码
|
||||
参数:
|
||||
code: 要删除的认证码
|
||||
返回:
|
||||
bool: 删除成功返回True,码不存在返回False
|
||||
"""
|
||||
print('remove_code', code)
|
||||
with self._lock:
|
||||
if code in self._used_codes:
|
||||
self._used_codes.remove(code)
|
||||
if code in self._code_timestamps:
|
||||
del self._code_timestamps[code]
|
||||
return True
|
||||
return False
|
||||
|
||||
def is_code_used(self, code: str) -> bool:
|
||||
"""
|
||||
检查认证码是否已被使用
|
||||
参数:
|
||||
code: 要检查的认证码
|
||||
返回:
|
||||
bool: 如果码存在返回True,否则返回False
|
||||
"""
|
||||
with self._lock:
|
||||
return code in self._used_codes
|
||||
|
||||
def clear_codes(self):
|
||||
"""清空所有已使用的认证码"""
|
||||
with self._lock:
|
||||
self._used_codes.clear()
|
||||
self._code_timestamps.clear()
|
||||
|
||||
def _clean_expired_codes(self):
|
||||
"""清理过期的认证码"""
|
||||
current_time = time.time()
|
||||
expired_codes = [
|
||||
code for code, timestamp in self._code_timestamps.items()
|
||||
if (current_time - timestamp) > self._code_timeout
|
||||
]
|
||||
for code in expired_codes:
|
||||
self._used_codes.remove(code)
|
||||
del self._code_timestamps[code]
|
||||
@@ -1,39 +0,0 @@
|
||||
import asyncio
|
||||
from typing import Dict
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
class FileLockManager:
|
||||
_instance = None
|
||||
_locks: Dict[str, asyncio.Lock] = {}
|
||||
|
||||
def __new__(cls):
|
||||
if cls._instance is None:
|
||||
cls._instance = super(FileLockManager, cls).__new__(cls)
|
||||
return cls._instance
|
||||
|
||||
@classmethod
|
||||
def get_lock(cls, file_path: str) -> asyncio.Lock:
|
||||
"""获取指定文件的锁"""
|
||||
if file_path not in cls._locks:
|
||||
cls._locks[file_path] = asyncio.Lock()
|
||||
return cls._locks[file_path]
|
||||
|
||||
@classmethod
|
||||
async def acquire_lock(cls, file_path: str):
|
||||
"""获取锁"""
|
||||
lock = cls.get_lock(file_path)
|
||||
await lock.acquire()
|
||||
logger.bind(tag=TAG).debug(f"Acquired lock for {file_path}")
|
||||
|
||||
@classmethod
|
||||
def release_lock(cls, file_path: str):
|
||||
"""释放锁"""
|
||||
if file_path in cls._locks:
|
||||
try:
|
||||
cls._locks[file_path].release()
|
||||
logger.bind(tag=TAG).debug(f"Released lock for {file_path}")
|
||||
except RuntimeError as e:
|
||||
logger.bind(tag=TAG).warning(f"Failed to release lock for {file_path}: {e}")
|
||||
@@ -2,16 +2,17 @@ import os
|
||||
import sys
|
||||
import importlib
|
||||
from config.logger import setup_logging
|
||||
from core.utils.util import read_config, get_project_dir
|
||||
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def create_instance(class_name, *args, **kwargs):
|
||||
if os.path.exists(os.path.join('core', 'providers', 'memory', class_name, f'{class_name}.py')):
|
||||
lib_name = f'core.providers.memory.{class_name}.{class_name}'
|
||||
if os.path.exists(
|
||||
os.path.join("core", "providers", "memory", class_name, f"{class_name}.py")
|
||||
):
|
||||
lib_name = f"core.providers.memory.{class_name}.{class_name}"
|
||||
if lib_name not in sys.modules:
|
||||
sys.modules[lib_name] = importlib.import_module(f'{lib_name}')
|
||||
sys.modules[lib_name] = importlib.import_module(f"{lib_name}")
|
||||
return sys.modules[lib_name].MemoryProvider(*args, **kwargs)
|
||||
|
||||
raise ValueError(f"不支持的记忆服务类型: {class_name}")
|
||||
|
||||
|
||||
@@ -1,19 +1,12 @@
|
||||
import os
|
||||
import json
|
||||
import yaml
|
||||
import socket
|
||||
import subprocess
|
||||
import logging
|
||||
import re
|
||||
import requests
|
||||
from typing import Dict, Any
|
||||
from core.utils import tts, llm, intent, memory, vad, asr
|
||||
|
||||
|
||||
def get_project_dir():
|
||||
"""获取项目根目录"""
|
||||
return (
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
+ "/"
|
||||
)
|
||||
TAG = __name__
|
||||
|
||||
|
||||
def get_local_ip():
|
||||
@@ -72,7 +65,7 @@ def is_private_ip(ip_addr):
|
||||
return False # IP address format error or insufficient segments
|
||||
|
||||
|
||||
def get_ip_info(ip_addr):
|
||||
def get_ip_info(ip_addr, logger):
|
||||
try:
|
||||
if is_private_ip(ip_addr):
|
||||
ip_addr = ""
|
||||
@@ -81,16 +74,10 @@ def get_ip_info(ip_addr):
|
||||
ip_info = {"city": resp.get("city")}
|
||||
return ip_info
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting client ip info: {e}")
|
||||
logger.bind(tag=TAG).error(f"Error getting client ip info: {e}")
|
||||
return {}
|
||||
|
||||
|
||||
def read_config(config_path):
|
||||
with open(config_path, "r", encoding="utf-8") as file:
|
||||
config = yaml.safe_load(file)
|
||||
return config
|
||||
|
||||
|
||||
def write_json_file(file_path, data):
|
||||
"""将数据写入 JSON 文件"""
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
@@ -169,10 +156,8 @@ def remove_punctuation_and_length(text):
|
||||
|
||||
def check_model_key(modelType, modelKey):
|
||||
if "你" in modelKey:
|
||||
logging.error(
|
||||
"你还没配置"
|
||||
+ modelType
|
||||
+ "的密钥,请在配置文件中配置密钥,否则无法正常工作"
|
||||
raise ValueError(
|
||||
"你还没配置" + modelType + "的密钥,请检查一下所使用的LLM是否配置了密钥"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
@@ -212,3 +197,105 @@ def extract_json_from_string(input_string):
|
||||
if match:
|
||||
return match.group(1) # 返回提取的 JSON 字符串
|
||||
return None
|
||||
|
||||
|
||||
def initialize_modules(
|
||||
logger,
|
||||
config: Dict[str, Any],
|
||||
init_vad=False,
|
||||
init_asr=False,
|
||||
init_llm=False,
|
||||
init_tts=False,
|
||||
init_memory=False,
|
||||
init_intent=False,
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
初始化所有模块组件
|
||||
|
||||
Args:
|
||||
config: 配置字典
|
||||
|
||||
Returns:
|
||||
Dict[str, Any]: 包含所有初始化后的模块的字典
|
||||
"""
|
||||
modules = {}
|
||||
|
||||
# 初始化TTS模块
|
||||
if init_tts:
|
||||
tts_type = (
|
||||
config["selected_module"]["TTS"]
|
||||
if "type" not in config["TTS"][config["selected_module"]["TTS"]]
|
||||
else config["TTS"][config["selected_module"]["TTS"]]["type"]
|
||||
)
|
||||
modules["tts"] = tts.create_instance(
|
||||
tts_type,
|
||||
config["TTS"][config["selected_module"]["TTS"]],
|
||||
config["delete_audio"],
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: tts成功")
|
||||
|
||||
# 初始化LLM模块
|
||||
if init_llm:
|
||||
llm_type = (
|
||||
config["selected_module"]["LLM"]
|
||||
if "type" not in config["LLM"][config["selected_module"]["LLM"]]
|
||||
else config["LLM"][config["selected_module"]["LLM"]]["type"]
|
||||
)
|
||||
modules["llm"] = llm.create_instance(
|
||||
llm_type,
|
||||
config["LLM"][config["selected_module"]["LLM"]],
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: llm成功")
|
||||
|
||||
# 初始化Intent模块
|
||||
if init_intent:
|
||||
intent_type = (
|
||||
config["selected_module"]["Intent"]
|
||||
if "type" not in config["Intent"][config["selected_module"]["Intent"]]
|
||||
else config["Intent"][config["selected_module"]["Intent"]]["type"]
|
||||
)
|
||||
modules["intent"] = intent.create_instance(
|
||||
intent_type,
|
||||
config["Intent"][config["selected_module"]["Intent"]],
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: intent成功")
|
||||
# 初始化Memory模块
|
||||
if init_memory:
|
||||
memory_type = (
|
||||
config["selected_module"]["Memory"]
|
||||
if "type" not in config["Memory"][config["selected_module"]["Memory"]]
|
||||
else config["Memory"][config["selected_module"]["Memory"]]["type"]
|
||||
)
|
||||
modules["memory"] = memory.create_instance(
|
||||
memory_type,
|
||||
config["Memory"][config["selected_module"]["Memory"]],
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: memory成功")
|
||||
|
||||
# 初始化VAD模块
|
||||
if init_vad:
|
||||
vad_type = (
|
||||
config["selected_module"]["VAD"]
|
||||
if "type" not in config["VAD"][config["selected_module"]["VAD"]]
|
||||
else config["VAD"][config["selected_module"]["VAD"]]["type"]
|
||||
)
|
||||
modules["vad"] = vad.create_instance(
|
||||
vad_type,
|
||||
config["VAD"][config["selected_module"]["VAD"]],
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: vad成功")
|
||||
# 初始化ASR模块
|
||||
if init_asr:
|
||||
asr_type = (
|
||||
config["selected_module"]["ASR"]
|
||||
if "type" not in config["ASR"][config["selected_module"]["ASR"]]
|
||||
else config["ASR"][config["selected_module"]["ASR"]]["type"]
|
||||
)
|
||||
modules["asr"] = asr.create_instance(
|
||||
asr_type,
|
||||
config["ASR"][config["selected_module"]["ASR"]],
|
||||
config["delete_audio"],
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: asr成功")
|
||||
|
||||
return modules
|
||||
|
||||
@@ -1,77 +1,19 @@
|
||||
from abc import ABC, abstractmethod
|
||||
import importlib
|
||||
import os
|
||||
import sys
|
||||
from core.providers.vad.base import VADProviderBase
|
||||
from config.logger import setup_logging
|
||||
import opuslib_next
|
||||
import time
|
||||
import numpy as np
|
||||
import torch
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
class VAD(ABC):
|
||||
@abstractmethod
|
||||
def is_vad(self, conn, data):
|
||||
"""检测音频数据中的语音活动"""
|
||||
pass
|
||||
|
||||
def create_instance(class_name: str, *args, **kwargs) -> VADProviderBase:
|
||||
"""工厂方法创建VAD实例"""
|
||||
if os.path.exists(os.path.join("core", "providers", "vad", f"{class_name}.py")):
|
||||
lib_name = f"core.providers.vad.{class_name}"
|
||||
if lib_name not in sys.modules:
|
||||
sys.modules[lib_name] = importlib.import_module(f"{lib_name}")
|
||||
return sys.modules[lib_name].VADProvider(*args, **kwargs)
|
||||
|
||||
class SileroVAD(VAD):
|
||||
def __init__(self, config):
|
||||
logger.bind(tag=TAG).info("SileroVAD", config)
|
||||
self.model, self.utils = torch.hub.load(repo_or_dir=config["model_dir"],
|
||||
source='local',
|
||||
model='silero_vad',
|
||||
force_reload=False)
|
||||
(get_speech_timestamps, _, _, _, _) = self.utils
|
||||
|
||||
self.decoder = opuslib_next.Decoder(16000, 1)
|
||||
self.vad_threshold = config.get("threshold")
|
||||
self.silence_threshold_ms = config.get("min_silence_duration_ms")
|
||||
|
||||
def is_vad(self, conn, opus_packet):
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(opus_packet, 960)
|
||||
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
||||
|
||||
# 处理缓冲区中的完整帧(每次处理512采样点)
|
||||
client_have_voice = False
|
||||
while len(conn.client_audio_buffer) >= 512 * 2:
|
||||
# 提取前512个采样点(1024字节)
|
||||
chunk = conn.client_audio_buffer[:512 * 2]
|
||||
conn.client_audio_buffer = conn.client_audio_buffer[512 * 2:]
|
||||
|
||||
# 转换为模型需要的张量格式
|
||||
audio_int16 = np.frombuffer(chunk, dtype=np.int16)
|
||||
audio_float32 = audio_int16.astype(np.float32) / 32768.0
|
||||
audio_tensor = torch.from_numpy(audio_float32)
|
||||
|
||||
# 检测语音活动
|
||||
speech_prob = self.model(audio_tensor, 16000).item()
|
||||
client_have_voice = speech_prob >= self.vad_threshold
|
||||
|
||||
# 如果之前有声音,但本次没有声音,且与上次有声音的时间查已经超过了静默阈值,则认为已经说完一句话
|
||||
if conn.client_have_voice and not client_have_voice:
|
||||
stop_duration = time.time() * 1000 - conn.client_have_voice_last_time
|
||||
if stop_duration >= self.silence_threshold_ms:
|
||||
conn.client_voice_stop = True
|
||||
if client_have_voice:
|
||||
conn.client_have_voice = True
|
||||
conn.client_have_voice_last_time = time.time() * 1000
|
||||
|
||||
return client_have_voice
|
||||
except opuslib_next.OpusError as e:
|
||||
logger.bind(tag=TAG).info(f"解码错误: {e}")
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"Error processing audio packet: {e}")
|
||||
|
||||
|
||||
def create_instance(class_name, *args, **kwargs) -> VAD:
|
||||
# 获取类对象
|
||||
cls_map = {
|
||||
"SileroVAD": SileroVAD,
|
||||
# 可扩展其他SileroVAD实现
|
||||
}
|
||||
|
||||
if cls := cls_map.get(class_name):
|
||||
return cls(*args, **kwargs)
|
||||
raise ValueError(f"不支持的SileroVAD类型: {class_name}")
|
||||
raise ValueError(f"不支持的VAD类型: {class_name},请检查该配置的type是否设置正确")
|
||||
|
||||
@@ -2,8 +2,7 @@ import asyncio
|
||||
import websockets
|
||||
from config.logger import setup_logging
|
||||
from core.connection import ConnectionHandler
|
||||
from core.utils.util import get_local_ip
|
||||
from core.utils import asr, vad, llm, tts, memory, intent
|
||||
from core.utils.util import get_local_ip, initialize_modules
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -12,74 +11,16 @@ class WebSocketServer:
|
||||
def __init__(self, config: dict):
|
||||
self.config = config
|
||||
self.logger = setup_logging()
|
||||
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = (
|
||||
self._create_processing_instances()
|
||||
)
|
||||
self.active_connections = set() # 添加全局连接记录
|
||||
|
||||
def _create_processing_instances(self):
|
||||
memory_cls_name = self.config["selected_module"].get(
|
||||
"Memory", "nomem"
|
||||
) # 默认使用nomem
|
||||
has_memory_cfg = (
|
||||
self.config.get("Memory") and memory_cls_name in self.config["Memory"]
|
||||
)
|
||||
memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {}
|
||||
|
||||
"""创建处理模块实例"""
|
||||
return (
|
||||
vad.create_instance(
|
||||
self.config["selected_module"]["VAD"],
|
||||
self.config["VAD"][self.config["selected_module"]["VAD"]],
|
||||
),
|
||||
asr.create_instance(
|
||||
(
|
||||
self.config["selected_module"]["ASR"]
|
||||
if not "type"
|
||||
in self.config["ASR"][self.config["selected_module"]["ASR"]]
|
||||
else self.config["ASR"][self.config["selected_module"]["ASR"]][
|
||||
"type"
|
||||
]
|
||||
),
|
||||
self.config["ASR"][self.config["selected_module"]["ASR"]],
|
||||
self.config["delete_audio"],
|
||||
),
|
||||
llm.create_instance(
|
||||
(
|
||||
self.config["selected_module"]["LLM"]
|
||||
if not "type"
|
||||
in self.config["LLM"][self.config["selected_module"]["LLM"]]
|
||||
else self.config["LLM"][self.config["selected_module"]["LLM"]][
|
||||
"type"
|
||||
]
|
||||
),
|
||||
self.config["LLM"][self.config["selected_module"]["LLM"]],
|
||||
),
|
||||
tts.create_instance(
|
||||
(
|
||||
self.config["selected_module"]["TTS"]
|
||||
if not "type"
|
||||
in self.config["TTS"][self.config["selected_module"]["TTS"]]
|
||||
else self.config["TTS"][self.config["selected_module"]["TTS"]][
|
||||
"type"
|
||||
]
|
||||
),
|
||||
self.config["TTS"][self.config["selected_module"]["TTS"]],
|
||||
self.config["delete_audio"],
|
||||
),
|
||||
memory.create_instance(memory_cls_name, memory_cfg),
|
||||
intent.create_instance(
|
||||
(
|
||||
self.config["selected_module"]["Intent"]
|
||||
if not "type"
|
||||
in self.config["Intent"][self.config["selected_module"]["Intent"]]
|
||||
else self.config["Intent"][
|
||||
self.config["selected_module"]["Intent"]
|
||||
]["type"]
|
||||
),
|
||||
self.config["Intent"][self.config["selected_module"]["Intent"]],
|
||||
),
|
||||
modules = initialize_modules(
|
||||
self.logger, self.config, True, True, True, True, True, True
|
||||
)
|
||||
self._vad = modules["vad"]
|
||||
self._asr = modules["asr"]
|
||||
self._tts = modules["tts"]
|
||||
self._llm = modules["llm"]
|
||||
self._intent = modules["intent"]
|
||||
self._memory = modules["memory"]
|
||||
self.active_connections = set()
|
||||
|
||||
async def start(self):
|
||||
server_config = self.config["server"]
|
||||
@@ -111,7 +52,7 @@ class WebSocketServer:
|
||||
self._llm,
|
||||
self._tts,
|
||||
self._memory,
|
||||
self.intent,
|
||||
self._intent,
|
||||
)
|
||||
self.active_connections.add(handler)
|
||||
try:
|
||||
|
||||
Reference in New Issue
Block a user