mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-28 10:03:54 +08:00
Merge branch 'main' into pcm
This commit is contained in:
@@ -18,6 +18,8 @@ from core.utils.util import (
|
||||
get_string_no_punctuation_or_emoji,
|
||||
extract_json_from_string,
|
||||
initialize_modules,
|
||||
check_vad_update,
|
||||
check_asr_update,
|
||||
)
|
||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||
from core.handle.sendAudioHandle import sendAudioMessage
|
||||
@@ -52,10 +54,12 @@ class ConnectionHandler:
|
||||
_intent,
|
||||
server=None,
|
||||
):
|
||||
self.config = config
|
||||
self.server = server
|
||||
self.common_config = config
|
||||
self.config = copy.deepcopy(config)
|
||||
self.session_id = str(uuid.uuid4())
|
||||
self.logger = setup_logging()
|
||||
self.auth = AuthMiddleware(config)
|
||||
self.server = server # 保存server实例的引用
|
||||
|
||||
self.need_bind = False
|
||||
self.bind_code = None
|
||||
@@ -66,7 +70,6 @@ class ConnectionHandler:
|
||||
self.device_id = None
|
||||
self.client_ip = None
|
||||
self.client_ip_info = {}
|
||||
self.session_id = None
|
||||
self.prompt = None
|
||||
self.welcome_msg = None
|
||||
self.max_output_size = 0
|
||||
@@ -87,8 +90,10 @@ class ConnectionHandler:
|
||||
self.tts_report_thread = None
|
||||
|
||||
# 依赖的组件
|
||||
self.vad = _vad
|
||||
self.asr = _asr
|
||||
self.vad = None
|
||||
self.asr = None
|
||||
self._asr = _asr
|
||||
self._vad = _vad
|
||||
self.llm = _llm
|
||||
self.tts = _tts
|
||||
self.memory = _memory
|
||||
@@ -153,11 +158,9 @@ class ConnectionHandler:
|
||||
self.headers["device-id"] = query_params["device-id"][0]
|
||||
self.headers["client-id"] = query_params["client-id"][0]
|
||||
else:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
"无法从请求头和URL查询参数中获取device-id"
|
||||
)
|
||||
await ws.send("端口正常,如需测试连接,请使用test_page.html")
|
||||
await self.close(ws)
|
||||
return
|
||||
|
||||
# 获取客户端ip地址
|
||||
self.client_ip = ws.remote_address[0]
|
||||
self.logger.bind(tag=TAG).info(
|
||||
@@ -170,7 +173,6 @@ class ConnectionHandler:
|
||||
# 认证通过,继续处理
|
||||
self.websocket = ws
|
||||
self.device_id = self.headers.get("device-id", None)
|
||||
self.session_id = str(uuid.uuid4())
|
||||
|
||||
# 启动超时检查任务
|
||||
self.timeout_task = asyncio.create_task(self._check_timeout())
|
||||
@@ -180,9 +182,9 @@ class ConnectionHandler:
|
||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
||||
|
||||
# 获取差异化配置
|
||||
private_config = self._initialize_private_config()
|
||||
self._initialize_private_config()
|
||||
# 异步初始化
|
||||
self.executor.submit(self._initialize_components, private_config)
|
||||
self.executor.submit(self._initialize_components)
|
||||
# tts 消化线程
|
||||
self.tts_priority_thread = threading.Thread(
|
||||
target=self._tts_priority_thread, daemon=True
|
||||
@@ -214,7 +216,8 @@ class ConnectionHandler:
|
||||
async def _save_and_close(self, ws):
|
||||
"""保存记忆并关闭连接"""
|
||||
try:
|
||||
await self.memory.save_memory(self.dialogue.dialogue)
|
||||
if self.memory:
|
||||
await self.memory.save_memory(self.dialogue.dialogue)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"保存记忆失败: {e}")
|
||||
finally:
|
||||
@@ -236,81 +239,17 @@ class ConnectionHandler:
|
||||
elif isinstance(message, bytes):
|
||||
await handleAudioMessage(self, message)
|
||||
|
||||
async def handle_config_update(self, message):
|
||||
"""处理配置更新请求"""
|
||||
content = message.get("content", {})
|
||||
new_config = content
|
||||
|
||||
# 遍历所有支持的配置模块
|
||||
updated_modules = []
|
||||
for config_model in ["tts", "llm", "vad", "asr", "memory", "intent"]:
|
||||
if config_model not in new_config:
|
||||
continue
|
||||
|
||||
new_content = new_config[config_model]
|
||||
old_content = self.config.get(config_model, {})
|
||||
|
||||
# 记录配置变更
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"配置更新: {config_model} 旧值: {json.dumps(old_content, ensure_ascii=False)} "
|
||||
f"新值: {json.dumps(new_content, ensure_ascii=False)}"
|
||||
)
|
||||
|
||||
# 深度合并配置
|
||||
if isinstance(old_content, dict) and isinstance(new_content, dict):
|
||||
merged = {**old_content, **new_content}
|
||||
self.config[config_model] = merged
|
||||
else:
|
||||
self.config[config_model] = new_content
|
||||
|
||||
# 标记需要重新初始化的模块
|
||||
if config_model in ["llm", "tts", "asr", "vad", "intent", "memory"]:
|
||||
updated_modules.append(config_model)
|
||||
|
||||
# 同步更新 WebSocketServer 的配置
|
||||
if self.server:
|
||||
async with self.server.config_lock: # 使用锁确保线程安全
|
||||
for config_model in updated_modules:
|
||||
self.server.config[config_model].update(new_config[config_model])
|
||||
|
||||
# 批量初始化模块
|
||||
if updated_modules:
|
||||
try:
|
||||
self._initialize_components(self.config)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"已重新初始化模块: {', '.join(updated_modules)}"
|
||||
)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"模块初始化失败: {str(e)}")
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"status": "error",
|
||||
"message": f"模块初始化失败: {str(e)}",
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# 返回成功响应
|
||||
await self.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"status": "success",
|
||||
"message": f"已更新配置: {', '.join(updated_modules)}",
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
def _initialize_components(self, private_config):
|
||||
def _initialize_components(self):
|
||||
"""初始化组件"""
|
||||
if private_config is not None:
|
||||
self._initialize_models(private_config)
|
||||
else:
|
||||
self.prompt = self.config["prompt"]
|
||||
self.change_system_prompt(self.prompt)
|
||||
self.prompt = self.config["prompt"]
|
||||
self.change_system_prompt(self.prompt)
|
||||
self.logger.bind(tag=TAG).info(f"初始化组件: prompt成功 {self.prompt[:50]}...")
|
||||
|
||||
"""初始化本地组件"""
|
||||
if self.vad is None:
|
||||
self.vad = self._vad
|
||||
if self.asr is None:
|
||||
self.asr = self._asr
|
||||
"""加载记忆"""
|
||||
self._initialize_memory()
|
||||
"""加载意图识别"""
|
||||
@@ -320,7 +259,7 @@ class ConnectionHandler:
|
||||
|
||||
def _init_report_threads(self):
|
||||
"""初始化ASR和TTS上报线程"""
|
||||
if not self.read_config_from_api:
|
||||
if not self.read_config_from_api or self.need_bind:
|
||||
return
|
||||
if self.tts_report_thread is None or not self.tts_report_thread.is_alive():
|
||||
self.tts_report_thread = threading.Thread(
|
||||
@@ -357,54 +296,21 @@ class ConnectionHandler:
|
||||
self.logger.bind(tag=TAG).error(f"获取差异化配置失败: {e}")
|
||||
private_config = {}
|
||||
|
||||
init_tts = False
|
||||
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"
|
||||
]
|
||||
|
||||
try:
|
||||
modules = initialize_modules(
|
||||
self.logger,
|
||||
private_config,
|
||||
False,
|
||||
False,
|
||||
False,
|
||||
init_tts,
|
||||
False,
|
||||
False,
|
||||
)
|
||||
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("prompt", None) is not None:
|
||||
self.change_system_prompt(modules["prompt"])
|
||||
private_config["prompt"] = None
|
||||
return private_config
|
||||
|
||||
def _initialize_models(self, private_config):
|
||||
init_vad, init_asr, init_llm, init_memory, init_intent = (
|
||||
False,
|
||||
init_llm, init_tts, init_memory, init_intent = (
|
||||
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"
|
||||
|
||||
init_vad = check_vad_update(self.common_config, private_config)
|
||||
init_asr = check_asr_update(self.common_config, private_config)
|
||||
|
||||
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("LLM", None) is not None:
|
||||
init_llm = True
|
||||
@@ -424,8 +330,11 @@ class ConnectionHandler:
|
||||
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"]
|
||||
if private_config.get("device_max_output_size", None) is not None:
|
||||
self.max_output_size = int(private_config["device_max_output_size"])
|
||||
|
||||
try:
|
||||
modules = initialize_modules(
|
||||
self.logger,
|
||||
@@ -433,13 +342,15 @@ class ConnectionHandler:
|
||||
init_vad,
|
||||
init_asr,
|
||||
init_llm,
|
||||
False,
|
||||
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("vad", None) is not None:
|
||||
self.vad = modules["vad"]
|
||||
if modules.get("asr", None) is not None:
|
||||
@@ -517,10 +428,12 @@ class ConnectionHandler:
|
||||
processed_chars = 0 # 跟踪已处理的字符位置
|
||||
try:
|
||||
# 使用带记忆的对话
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.memory.query_memory(query), self.loop
|
||||
)
|
||||
memory_str = future.result()
|
||||
memory_str = None
|
||||
if self.memory is not None:
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.memory.query_memory(query), self.loop
|
||||
)
|
||||
memory_str = future.result()
|
||||
|
||||
self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
|
||||
llm_responses = self.llm.response(
|
||||
@@ -567,7 +480,7 @@ class ConnectionHandler:
|
||||
future = self.executor.submit(
|
||||
self.speak_and_play, segment_text, text_index
|
||||
)
|
||||
self.tts_queue.put(future)
|
||||
self.tts_queue.put((future, text_index))
|
||||
processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
||||
|
||||
# 处理最后剩余的文本
|
||||
@@ -581,7 +494,7 @@ class ConnectionHandler:
|
||||
future = self.executor.submit(
|
||||
self.speak_and_play, segment_text, text_index
|
||||
)
|
||||
self.tts_queue.put(future)
|
||||
self.tts_queue.put((future, text_index))
|
||||
|
||||
self.llm_finish_task = True
|
||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
||||
@@ -608,10 +521,12 @@ class ConnectionHandler:
|
||||
start_time = time.time()
|
||||
|
||||
# 使用带记忆的对话
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.memory.query_memory(query), self.loop
|
||||
)
|
||||
memory_str = future.result()
|
||||
memory_str = None
|
||||
if self.memory is not None:
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.memory.query_memory(query), self.loop
|
||||
)
|
||||
memory_str = future.result()
|
||||
|
||||
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
|
||||
|
||||
@@ -697,7 +612,7 @@ class ConnectionHandler:
|
||||
future = self.executor.submit(
|
||||
self.speak_and_play, segment_text, text_index
|
||||
)
|
||||
self.tts_queue.put(future)
|
||||
self.tts_queue.put((future, text_index))
|
||||
# 更新已处理字符位置
|
||||
processed_chars += len(segment_text_raw)
|
||||
|
||||
@@ -756,7 +671,7 @@ class ConnectionHandler:
|
||||
future = self.executor.submit(
|
||||
self.speak_and_play, segment_text, text_index
|
||||
)
|
||||
self.tts_queue.put(future)
|
||||
self.tts_queue.put((future, text_index))
|
||||
|
||||
# 存储对话内容
|
||||
if len(response_message) > 0:
|
||||
@@ -818,7 +733,7 @@ class ConnectionHandler:
|
||||
text = result.response
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.tts_queue.put((future, text_index))
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||
text = result.result
|
||||
@@ -851,7 +766,7 @@ class ConnectionHandler:
|
||||
text = result.result
|
||||
self.recode_first_last_text(text, text_index)
|
||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||
self.tts_queue.put(future)
|
||||
self.tts_queue.put((future, text_index))
|
||||
self.dialogue.put(Message(role="assistant", content=text))
|
||||
else:
|
||||
pass
|
||||
@@ -861,7 +776,10 @@ class ConnectionHandler:
|
||||
text = None
|
||||
try:
|
||||
try:
|
||||
future = self.tts_queue.get(timeout=1)
|
||||
item = self.tts_queue.get(timeout=1)
|
||||
if item is None:
|
||||
continue
|
||||
future, text_index = item # 解包获取 Future 和 text_index
|
||||
except queue.Empty:
|
||||
if self.stop_event.is_set():
|
||||
break
|
||||
@@ -869,11 +787,11 @@ class ConnectionHandler:
|
||||
if future is None:
|
||||
continue
|
||||
text = None
|
||||
audio_datas, text_index, tts_file = [], 0, None
|
||||
opus_datas, tts_file = [], None
|
||||
try:
|
||||
self.logger.bind(tag=TAG).debug("正在处理TTS任务...")
|
||||
tts_timeout = int(self.config.get("tts_timeout", 10))
|
||||
tts_file, text, text_index = future.result(timeout=tts_timeout)
|
||||
tts_file, text, _ = future.result(timeout=tts_timeout)
|
||||
if text is None or len(text) <= 0:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
f"TTS出错:{text_index}: tts text is empty"
|
||||
@@ -1096,6 +1014,7 @@ def filter_sensitive_info(config: dict) -> dict:
|
||||
"personal_access_token",
|
||||
"access_token",
|
||||
"token",
|
||||
"secret",
|
||||
"access_key_secret",
|
||||
"secret_key",
|
||||
]
|
||||
|
||||
@@ -3,15 +3,16 @@ import queue
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def handleAbortMessage(conn):
|
||||
logger.bind(tag=TAG).info("Abort message received")
|
||||
conn.logger.bind(tag=TAG).info("Abort message received")
|
||||
# 设置成打断状态,会自动打断llm、tts任务
|
||||
conn.client_abort = True
|
||||
conn.clear_queues()
|
||||
# 打断客户端说话状态
|
||||
await conn.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": conn.session_id}))
|
||||
await conn.websocket.send(
|
||||
json.dumps({"type": "tts", "state": "stop", "session_id": conn.session_id})
|
||||
)
|
||||
conn.clearSpeakStatus()
|
||||
logger.bind(tag=TAG).info("Abort message received-end")
|
||||
conn.logger.bind(tag=TAG).info("Abort message received-end")
|
||||
|
||||
@@ -4,7 +4,6 @@ from plugins_func.register import FunctionRegistry, ActionResponse, Action, Tool
|
||||
from plugins_func.functions.hass_init import append_devices_to_prompt
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
class FunctionHandler:
|
||||
@@ -40,7 +39,9 @@ class FunctionHandler:
|
||||
for func in self.functions_desc:
|
||||
func_names.append(func["function"]["name"])
|
||||
# 打印当前支持的函数列表
|
||||
logger.bind(tag=TAG).info(f"当前支持的函数列表: {func_names}")
|
||||
self.conn.logger.bind(tag=TAG, session_id=self.conn.session_id).info(
|
||||
f"当前支持的函数列表: {func_names}"
|
||||
)
|
||||
return func_names
|
||||
|
||||
def get_functions(self):
|
||||
@@ -79,7 +80,9 @@ class FunctionHandler:
|
||||
func = funcItem.func
|
||||
arguments = function_call_data["arguments"]
|
||||
arguments = json.loads(arguments) if arguments else {}
|
||||
logger.bind(tag=TAG).debug(f"调用函数: {function_name}, 参数: {arguments}")
|
||||
self.conn.logger.bind(tag=TAG).debug(
|
||||
f"调用函数: {function_name}, 参数: {arguments}"
|
||||
)
|
||||
if (
|
||||
funcItem.type == ToolType.SYSTEM_CTL
|
||||
or funcItem.type == ToolType.IOT_CTL
|
||||
@@ -94,6 +97,6 @@ class FunctionHandler:
|
||||
action=Action.NOTFOUND, result="没有找到对应的函数", response=""
|
||||
)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||
self.conn.logger.bind(tag=TAG).error(f"处理function call错误: {e}")
|
||||
|
||||
return None
|
||||
|
||||
@@ -9,7 +9,6 @@ import random
|
||||
import time
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
WAKEUP_CONFIG = {
|
||||
"dir": "config/assets/",
|
||||
@@ -53,7 +52,7 @@ async def checkWakeupWords(conn, text):
|
||||
if file is None:
|
||||
asyncio.create_task(wakeupWordsResponse(conn))
|
||||
return False
|
||||
opus_packets, duration = conn.tts.audio_to_opus_data(file)
|
||||
opus_packets, _ = conn.tts.audio_to_opus_data(file)
|
||||
text_hello = WAKEUP_CONFIG["text"]
|
||||
if not text_hello:
|
||||
text_hello = text
|
||||
@@ -84,7 +83,7 @@ async def wakeupWordsResponse(conn):
|
||||
await asyncio.sleep(1)
|
||||
wait_max_time -= 1
|
||||
if wait_max_time <= 0:
|
||||
logger.bind(tag=TAG).error("连接对象没有llm")
|
||||
conn.logger.bind(tag=TAG).error("连接对象没有llm")
|
||||
return
|
||||
|
||||
"""唤醒词响应"""
|
||||
|
||||
@@ -5,10 +5,8 @@ from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.handle.helloHandle import checkWakeupWords
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
from core.utils.dialogue import Message
|
||||
from loguru import logger
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def handle_user_intent(conn, text):
|
||||
@@ -36,7 +34,7 @@ async def check_direct_exit(conn, text):
|
||||
cmd_exit = conn.cmd_exit
|
||||
for cmd in cmd_exit:
|
||||
if text == cmd:
|
||||
logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
|
||||
conn.logger.bind(tag=TAG).info(f"识别到明确的退出命令: {text}")
|
||||
await send_stt_message(conn, text)
|
||||
await conn.close()
|
||||
return True
|
||||
@@ -46,7 +44,7 @@ async def check_direct_exit(conn, text):
|
||||
async def analyze_intent_with_llm(conn, text):
|
||||
"""使用LLM分析用户意图"""
|
||||
if not hasattr(conn, "intent") or not conn.intent:
|
||||
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||
conn.logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||
return None
|
||||
|
||||
# 对话历史记录
|
||||
@@ -55,7 +53,7 @@ async def analyze_intent_with_llm(conn, text):
|
||||
intent_result = await conn.intent.detect_intent(conn, dialogue.dialogue, text)
|
||||
return intent_result
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
||||
conn.logger.bind(tag=TAG).error(f"意图识别失败: {str(e)}")
|
||||
|
||||
return None
|
||||
|
||||
@@ -69,7 +67,7 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
# 检查是否有function_call
|
||||
if "function_call" in intent_data:
|
||||
# 直接从意图识别获取了function_call
|
||||
logger.bind(tag=TAG).debug(
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
|
||||
)
|
||||
function_name = intent_data["function_call"]["name"]
|
||||
@@ -118,7 +116,7 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
conn.speak_and_play, text, text_index
|
||||
)
|
||||
conn.llm_finish_task = True
|
||||
conn.tts_queue.put(future)
|
||||
conn.tts_queue.put((future, text_index))
|
||||
conn.dialogue.put(Message(role="assistant", content=text))
|
||||
|
||||
# 将函数执行放在线程池中
|
||||
@@ -126,7 +124,7 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
return True
|
||||
return False
|
||||
except json.JSONDecodeError as e:
|
||||
logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
|
||||
conn.logger.bind(tag=TAG).error(f"处理意图结果时出错: {e}")
|
||||
return False
|
||||
|
||||
|
||||
|
||||
@@ -10,7 +10,6 @@ from plugins_func.register import (
|
||||
)
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def wrap_async_function(async_func):
|
||||
@@ -21,7 +20,7 @@ def wrap_async_function(async_func):
|
||||
# 获取连接对象(第一个参数)
|
||||
conn = args[0]
|
||||
if not hasattr(conn, "loop"):
|
||||
logger.bind(tag=TAG).error("Connection对象没有loop属性")
|
||||
conn.logger.bind(tag=TAG).error("Connection对象没有loop属性")
|
||||
return ActionResponse(
|
||||
Action.ERROR,
|
||||
"Connection对象没有loop属性",
|
||||
@@ -35,7 +34,7 @@ def wrap_async_function(async_func):
|
||||
# 等待结果返回
|
||||
return future.result()
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"运行异步函数时出错: {e}")
|
||||
conn.logger.bind(tag=TAG).error(f"运行异步函数时出错: {e}")
|
||||
return ActionResponse(Action.ERROR, str(e), f"执行操作时出错: {e}")
|
||||
|
||||
return wrapper
|
||||
@@ -57,7 +56,7 @@ def create_iot_function(device_name, method_name, method_info):
|
||||
response_failure = "操作失败"
|
||||
|
||||
# 打印响应参数
|
||||
logger.bind(tag=TAG).debug(
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
|
||||
)
|
||||
|
||||
@@ -86,7 +85,9 @@ def create_iot_function(device_name, method_name, method_info):
|
||||
|
||||
return ActionResponse(Action.RESPONSE, result, response)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"执行{device_name}的{method_name}操作失败: {e}")
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"执行{device_name}的{method_name}操作失败: {e}"
|
||||
)
|
||||
|
||||
# 操作失败时使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
@@ -104,7 +105,7 @@ def create_iot_query_function(device_name, prop_name, prop_info):
|
||||
async def iot_query_function(conn, response_success=None, response_failure=None):
|
||||
try:
|
||||
# 打印响应参数
|
||||
logger.bind(tag=TAG).info(
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"查询函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
|
||||
)
|
||||
|
||||
@@ -122,7 +123,9 @@ def create_iot_query_function(device_name, prop_name, prop_info):
|
||||
|
||||
return ActionResponse(Action.ERROR, f"属性{prop_name}不存在", response)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"查询{device_name}的{prop_name}时出错: {e}")
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"查询{device_name}的{prop_name}时出错: {e}"
|
||||
)
|
||||
|
||||
# 查询出错时使用大模型提供的失败响应
|
||||
response = response_failure
|
||||
@@ -280,7 +283,7 @@ async def handleIotDescriptors(conn, descriptors):
|
||||
await asyncio.sleep(1)
|
||||
wait_max_time -= 1
|
||||
if wait_max_time <= 0:
|
||||
logger.bind(tag=TAG).debug("连接对象没有func_handler")
|
||||
conn.logger.bind(tag=TAG).debug("连接对象没有func_handler")
|
||||
return
|
||||
"""处理物联网描述"""
|
||||
functions_changed = False
|
||||
@@ -323,7 +326,7 @@ async def handleIotDescriptors(conn, descriptors):
|
||||
if hasattr(conn, "func_handler"):
|
||||
for func_name in device_functions:
|
||||
conn.func_handler.function_registry.register_function(func_name)
|
||||
logger.bind(tag=TAG).info(
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"注册IOT函数到function handler: {func_name}"
|
||||
)
|
||||
functions_changed = True
|
||||
@@ -332,8 +335,8 @@ async def handleIotDescriptors(conn, descriptors):
|
||||
if functions_changed and hasattr(conn, "func_handler"):
|
||||
conn.func_handler.upload_functions_desc()
|
||||
func_names = conn.func_handler.current_support_functions()
|
||||
logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
||||
logger.bind(tag=TAG).info(
|
||||
conn.logger.bind(tag=TAG).info(f"设备类型: {type_id}")
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"更新function描述列表完成,当前支持的函数: {func_names}"
|
||||
)
|
||||
|
||||
@@ -347,13 +350,13 @@ async def handleIotStatus(conn, states):
|
||||
for k, v in state["state"].items():
|
||||
if property_item["name"] == k:
|
||||
if type(v) != type(property_item["value"]):
|
||||
logger.bind(tag=TAG).error(
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"属性{property_item['name']}的值类型不匹配"
|
||||
)
|
||||
break
|
||||
else:
|
||||
property_item["value"] = v
|
||||
logger.bind(tag=TAG).info(
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"物联网状态更新: {key} , {property_item['name']} = {v}"
|
||||
)
|
||||
break
|
||||
@@ -367,7 +370,7 @@ async def get_iot_status(conn, name, property_name):
|
||||
for property_item in value.properties:
|
||||
if property_item["name"] == property_name:
|
||||
return property_item["value"]
|
||||
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
conn.logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
return None
|
||||
|
||||
|
||||
@@ -378,16 +381,16 @@ async def set_iot_status(conn, name, property_name, value):
|
||||
for property_item in iot_descriptor.properties:
|
||||
if property_item["name"] == property_name:
|
||||
if type(value) != type(property_item["value"]):
|
||||
logger.bind(tag=TAG).error(
|
||||
conn.logger.bind(tag=TAG).error(
|
||||
f"属性{property_item['name']}的值类型不匹配"
|
||||
)
|
||||
return
|
||||
property_item["value"] = value
|
||||
logger.bind(tag=TAG).info(
|
||||
conn.logger.bind(tag=TAG).info(
|
||||
f"物联网状态更新: {name} , {property_name} = {value}"
|
||||
)
|
||||
return
|
||||
logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
conn.logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
|
||||
|
||||
|
||||
async def send_iot_conn(conn, name, method_name, parameters):
|
||||
@@ -409,6 +412,6 @@ async def send_iot_conn(conn, name, method_name, parameters):
|
||||
command["parameters"] = parameters
|
||||
send_message = json.dumps({"type": "iot", "commands": [command]})
|
||||
await conn.websocket.send(send_message)
|
||||
logger.bind(tag=TAG).info(f"发送物联网指令: {send_message}")
|
||||
conn.logger.bind(tag=TAG).info(f"发送物联网指令: {send_message}")
|
||||
return
|
||||
logger.bind(tag=TAG).error(f"未找到方法{method_name}")
|
||||
conn.logger.bind(tag=TAG).error(f"未找到方法{method_name}")
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from config.logger import setup_logging
|
||||
import time
|
||||
import copy
|
||||
from core.utils.util import remove_punctuation_and_length
|
||||
@@ -6,14 +5,16 @@ from core.handle.sendAudioHandle import send_stt_message
|
||||
from core.handle.intentHandler import handle_user_intent
|
||||
from core.utils.output_counter import check_device_output_limit
|
||||
from core.handle.ttsReportHandle import enqueue_tts_report
|
||||
from core.providers.tts.base import audio_to_opus_data
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def handleAudioMessage(conn, audio):
|
||||
if conn.vad is None:
|
||||
return
|
||||
if not conn.asr_server_receive:
|
||||
logger.bind(tag=TAG).debug(f"前期数据处理中,暂停接收")
|
||||
conn.logger.bind(tag=TAG).debug(f"前期数据处理中,暂停接收")
|
||||
return
|
||||
if conn.client_listen_mode == "auto":
|
||||
have_voice = conn.vad.is_vad(conn, audio)
|
||||
@@ -39,7 +40,7 @@ async def handleAudioMessage(conn, audio):
|
||||
conn.asr_server_receive = True
|
||||
else:
|
||||
text, _ = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
||||
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
||||
conn.logger.bind(tag=TAG).info(f"识别文本: {text}")
|
||||
text_len, _ = remove_punctuation_and_length(text)
|
||||
if text_len > 0:
|
||||
# 使用自定义模块进行上报
|
||||
@@ -110,7 +111,7 @@ async def max_out_size(conn):
|
||||
conn.tts_last_text_index = 0
|
||||
conn.llm_finish_task = True
|
||||
file_path = "config/assets/max_output_size.wav"
|
||||
opus_packets, _ = conn.tts.audio_to_opus_data(file_path)
|
||||
opus_packets, _ = audio_to_opus_data(file_path)
|
||||
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||
conn.close_after_chat = True
|
||||
|
||||
@@ -119,7 +120,7 @@ async def check_bind_device(conn):
|
||||
if conn.bind_code:
|
||||
# 确保bind_code是6位数字
|
||||
if len(conn.bind_code) != 6:
|
||||
logger.bind(tag=TAG).error(f"无效的绑定码格式: {conn.bind_code}")
|
||||
conn.logger.bind(tag=TAG).error(f"无效的绑定码格式: {conn.bind_code}")
|
||||
text = "绑定码格式错误,请检查配置。"
|
||||
await send_stt_message(conn, text)
|
||||
return
|
||||
@@ -132,7 +133,7 @@ async def check_bind_device(conn):
|
||||
|
||||
# 播放提示音
|
||||
music_path = "config/assets/bind_code.wav"
|
||||
opus_packets, _ = conn.tts.audio_to_opus_data(music_path)
|
||||
opus_packets, _ = audio_to_opus_data(music_path)
|
||||
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||
|
||||
# 逐个播放数字
|
||||
@@ -140,10 +141,10 @@ async def check_bind_device(conn):
|
||||
try:
|
||||
digit = conn.bind_code[i]
|
||||
num_path = f"config/assets/bind_code/{digit}.wav"
|
||||
num_packets, _ = conn.tts.audio_to_opus_data(num_path)
|
||||
num_packets, _ = audio_to_opus_data(num_path)
|
||||
conn.audio_play_queue.put((num_packets, None, i + 1))
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
|
||||
conn.logger.bind(tag=TAG).error(f"播放数字音频失败: {e}")
|
||||
continue
|
||||
else:
|
||||
text = f"没有找到该设备的版本信息,请正确配置 OTA地址,然后重新编译固件。"
|
||||
@@ -152,5 +153,5 @@ async def check_bind_device(conn):
|
||||
conn.tts_last_text_index = 0
|
||||
conn.llm_finish_task = True
|
||||
music_path = "config/assets/bind_not_found.wav"
|
||||
opus_packets, _ = conn.tts.audio_to_opus_data(music_path)
|
||||
opus_packets, _ = audio_to_opus_data(music_path)
|
||||
conn.audio_play_queue.put((opus_packets, text, 0))
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
from config.logger import setup_logging
|
||||
import json
|
||||
import asyncio
|
||||
import time
|
||||
from core.utils.util import get_string_no_punctuation_or_emoji, analyze_emotion
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
emoji_map = {
|
||||
"neutral": "😶",
|
||||
@@ -49,10 +47,10 @@ async def sendAudioMessage(conn, audios, text, text_index=0):
|
||||
)
|
||||
|
||||
if text_index == conn.tts_first_text_index:
|
||||
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
||||
conn.logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
||||
await send_tts_message(conn, "sentence_start", text)
|
||||
|
||||
is_first_audio = (text_index == conn.tts_first_text_index)
|
||||
is_first_audio = text_index == conn.tts_first_text_index
|
||||
await sendAudio(conn, audios, pre_buffer=is_first_audio)
|
||||
|
||||
await send_tts_message(conn, "sentence_end", text)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
from config.logger import setup_logging
|
||||
import json
|
||||
from core.handle.abortHandle import handleAbortMessage
|
||||
from core.handle.helloHandle import handleHelloMessage
|
||||
@@ -10,12 +9,11 @@ from core.handle.ttsReportHandle import enqueue_tts_report
|
||||
import asyncio
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
async def handleTextMessage(conn, message):
|
||||
"""处理文本消息"""
|
||||
logger.bind(tag=TAG).info(f"收到文本消息:{message}")
|
||||
conn.logger.bind(tag=TAG).info(f"收到文本消息:{message}")
|
||||
try:
|
||||
msg_json = json.loads(message)
|
||||
if isinstance(msg_json, int):
|
||||
@@ -28,7 +26,9 @@ async def handleTextMessage(conn, message):
|
||||
elif msg_json["type"] == "listen":
|
||||
if "mode" in msg_json:
|
||||
conn.client_listen_mode = msg_json["mode"]
|
||||
logger.bind(tag=TAG).debug(f"客户端拾音模式:{conn.client_listen_mode}")
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"客户端拾音模式:{conn.client_listen_mode}"
|
||||
)
|
||||
if msg_json["state"] == "start":
|
||||
conn.client_have_voice = True
|
||||
conn.client_voice_stop = False
|
||||
@@ -80,7 +80,7 @@ async def handleTextMessage(conn, message):
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"type": "server",
|
||||
"status": "error",
|
||||
"message": "服务器密钥验证失败",
|
||||
}
|
||||
@@ -89,6 +89,52 @@ async def handleTextMessage(conn, message):
|
||||
return
|
||||
# 动态更新配置
|
||||
if msg_json["action"] == "update_config":
|
||||
await conn.handle_config_update(msg_json)
|
||||
try:
|
||||
# 更新WebSocketServer的配置
|
||||
if not conn.server:
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"status": "error",
|
||||
"message": "无法获取服务器实例",
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if not await conn.server.update_config():
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"status": "error",
|
||||
"message": "更新服务器配置失败",
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
# 发送成功响应
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"status": "success",
|
||||
"message": "配置更新成功",
|
||||
}
|
||||
)
|
||||
)
|
||||
except Exception as e:
|
||||
conn.logger.bind(tag=TAG).error(f"更新配置失败: {str(e)}")
|
||||
await conn.websocket.send(
|
||||
json.dumps(
|
||||
{
|
||||
"type": "config_update_response",
|
||||
"status": "error",
|
||||
"message": f"更新配置失败: {str(e)}",
|
||||
}
|
||||
)
|
||||
)
|
||||
except json.JSONDecodeError:
|
||||
await conn.websocket.send(message)
|
||||
|
||||
@@ -9,16 +9,11 @@ TTS上报功能已集成到ConnectionHandler类中。
|
||||
具体实现请参考core/connection.py中的相关代码。
|
||||
"""
|
||||
|
||||
import os
|
||||
import uuid
|
||||
import wave
|
||||
import opuslib_next
|
||||
|
||||
from config.logger import setup_logging
|
||||
from config.manage_api_client import report
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
|
||||
|
||||
def report_tts(conn, type, text, opus_data):
|
||||
@@ -32,7 +27,7 @@ def report_tts(conn, type, text, opus_data):
|
||||
"""
|
||||
try:
|
||||
if opus_data:
|
||||
audio_data = opus_to_wav(opus_data)
|
||||
audio_data = opus_to_wav(conn, opus_data)
|
||||
else:
|
||||
audio_data = None
|
||||
# 执行上报
|
||||
@@ -44,10 +39,10 @@ def report_tts(conn, type, text, opus_data):
|
||||
audio=audio_data,
|
||||
)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"TTS上报失败: {e}")
|
||||
conn.logger.bind(tag=TAG).error(f"TTS上报失败: {e}")
|
||||
|
||||
|
||||
def opus_to_wav(opus_data):
|
||||
def opus_to_wav(conn, opus_data):
|
||||
"""将Opus数据转换为WAV格式的字节流
|
||||
|
||||
Args:
|
||||
@@ -65,7 +60,7 @@ def opus_to_wav(opus_data):
|
||||
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
||||
pcm_data.append(pcm_frame)
|
||||
except opuslib_next.OpusError as e:
|
||||
logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
||||
conn.logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
||||
|
||||
if not pcm_data:
|
||||
raise ValueError("没有有效的PCM数据")
|
||||
@@ -95,7 +90,7 @@ def opus_to_wav(opus_data):
|
||||
|
||||
|
||||
def enqueue_tts_report(conn, type, text, opus_data):
|
||||
if not conn.read_config_from_api:
|
||||
if not conn.read_config_from_api or conn.need_bind:
|
||||
return
|
||||
"""将TTS数据加入上报队列
|
||||
|
||||
@@ -108,8 +103,8 @@ def enqueue_tts_report(conn, type, text, opus_data):
|
||||
# 使用连接对象的队列,传入文本和二进制数据而非文件路径
|
||||
conn.tts_report_queue.put((type, text, opus_data))
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
conn.logger.bind(tag=TAG).debug(
|
||||
f"TTS数据已加入上报队列: {conn.device_id}, 音频大小: {len(opus_data)} "
|
||||
)
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
|
||||
conn.logger.bind(tag=TAG).error(f"加入TTS上报队列失败: {text}, {e}")
|
||||
|
||||
@@ -1,86 +1,125 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
from typing import Optional
|
||||
import asyncio, os, shutil, concurrent.futures
|
||||
from contextlib import AsyncExitStack
|
||||
import os, shutil
|
||||
from typing import Optional, List, Dict, Any
|
||||
|
||||
from mcp import ClientSession, StdioServerParameters
|
||||
from mcp.client.stdio import stdio_client
|
||||
|
||||
from mcp.client.sse import sse_client
|
||||
from config.logger import setup_logging
|
||||
|
||||
TAG = __name__
|
||||
|
||||
|
||||
class MCPClient:
|
||||
def __init__(self, config):
|
||||
# Initialize session and client objects
|
||||
self.session: Optional[ClientSession] = None
|
||||
self.exit_stack = AsyncExitStack()
|
||||
def __init__(self, config: Dict[str, Any]):
|
||||
self.logger = setup_logging()
|
||||
self.config = config
|
||||
self.tolls = []
|
||||
|
||||
self._worker_task: Optional[asyncio.Task] = None
|
||||
self._ready_evt = asyncio.Event()
|
||||
self._shutdown_evt = asyncio.Event()
|
||||
|
||||
self.session: Optional[ClientSession] = None
|
||||
self.tools: List = []
|
||||
|
||||
async def initialize(self):
|
||||
args = self.config.get("args", [])
|
||||
if self._worker_task:
|
||||
return
|
||||
self._worker_task = asyncio.create_task(self._worker(), name="MCPClientWorker")
|
||||
await self._ready_evt.wait()
|
||||
|
||||
command = (
|
||||
shutil.which("npx")
|
||||
if self.config["command"] == "npx"
|
||||
else self.config["command"]
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"Connected, tools = {[t.name for t in self.tools]}"
|
||||
)
|
||||
|
||||
env={**os.environ}
|
||||
if self.config.get("env"):
|
||||
env.update(self.config["env"])
|
||||
|
||||
server_params = StdioServerParameters(
|
||||
command=command,
|
||||
args=args,
|
||||
env=env
|
||||
)
|
||||
|
||||
stdio_transport = await self.exit_stack.enter_async_context(stdio_client(server_params))
|
||||
self.stdio, self.write = stdio_transport
|
||||
time_out_delta = timedelta(seconds=15)
|
||||
self.session = await self.exit_stack.enter_async_context(ClientSession(read_stream=self.stdio, write_stream=self.write, read_timeout_seconds=time_out_delta))
|
||||
|
||||
await self.session.initialize()
|
||||
|
||||
# List available tools
|
||||
response = await self.session.list_tools()
|
||||
tools = response.tools
|
||||
self.tools = tools
|
||||
self.logger.bind(tag=TAG).info(f"Connected to server with tools:{[tool.name for tool in tools]}")
|
||||
|
||||
def has_tool(self, tool_name):
|
||||
return any(tool.name == tool_name for tool in self.tools)
|
||||
|
||||
def get_available_tools(self):
|
||||
available_tools = [{"type": "function", "function":{
|
||||
"name": tool.name,
|
||||
"description": tool.description,
|
||||
"parameters": tool.inputSchema
|
||||
} } for tool in self.tools]
|
||||
|
||||
return available_tools
|
||||
|
||||
async def call_tool(self, tool_name: str, tool_args: dict):
|
||||
self.logger.bind(tag=TAG).info(f"MCPClient Calling tool {tool_name} with args: {tool_args}")
|
||||
try:
|
||||
response = await self.session.call_tool(tool_name, tool_args)
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"Error calling tool {tool_name}: {e}")
|
||||
from types import SimpleNamespace
|
||||
error_content = SimpleNamespace(
|
||||
type='text',
|
||||
text=f"Error calling tool {tool_name}: {e}"
|
||||
)
|
||||
error_response = SimpleNamespace(
|
||||
content=[error_content],
|
||||
isError=True
|
||||
)
|
||||
return error_response
|
||||
self.logger.bind(tag=TAG).info(f"MCPClient Response from tool {tool_name}: {response}")
|
||||
return response
|
||||
|
||||
async def cleanup(self):
|
||||
"""Clean up resources"""
|
||||
await self.exit_stack.aclose()
|
||||
if not self._worker_task:
|
||||
return
|
||||
|
||||
self._shutdown_evt.set()
|
||||
try:
|
||||
await asyncio.wait_for(self._worker_task, timeout=20)
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
self.logger.bind(tag=TAG).error(f"worker shutdown err: {e}")
|
||||
finally:
|
||||
self._worker_task = None
|
||||
|
||||
def has_tool(self, name: str) -> bool:
|
||||
return any(t.name == name for t in self.tools)
|
||||
|
||||
def get_available_tools(self):
|
||||
return [
|
||||
{
|
||||
"type": "function",
|
||||
"function": {
|
||||
"name": t.name,
|
||||
"description": t.description,
|
||||
"parameters": t.inputSchema,
|
||||
},
|
||||
}
|
||||
for t in self.tools
|
||||
]
|
||||
|
||||
async def call_tool(self, name: str, args: dict):
|
||||
if not self.session:
|
||||
raise RuntimeError("MCPClient not initialized")
|
||||
|
||||
loop = self._worker_task.get_loop()
|
||||
coro = self.session.call_tool(name, args)
|
||||
|
||||
if loop is asyncio.get_running_loop():
|
||||
return await coro
|
||||
|
||||
fut: concurrent.futures.Future = asyncio.run_coroutine_threadsafe(coro, loop)
|
||||
return await asyncio.wrap_future(fut)
|
||||
|
||||
async def _worker(self):
|
||||
async with AsyncExitStack() as stack:
|
||||
try:
|
||||
# 建立 StdioClient
|
||||
if "command" in self.config:
|
||||
cmd = (
|
||||
shutil.which("npx")
|
||||
if self.config["command"] == "npx"
|
||||
else self.config["command"]
|
||||
)
|
||||
env = {**os.environ, **self.config.get("env", {})}
|
||||
params = StdioServerParameters(
|
||||
command=cmd,
|
||||
args=self.config.get("args", []),
|
||||
env=env,
|
||||
)
|
||||
stdio_r, stdio_w = await stack.enter_async_context(stdio_client(params))
|
||||
read_stream, write_stream = stdio_r, stdio_w
|
||||
# 建立SSEClient
|
||||
elif "url" in self.config:
|
||||
sse_r, sse_w = await stack.enter_async_context(sse_client(self.config["url"]))
|
||||
read_stream, write_stream = sse_r, sse_w
|
||||
|
||||
else:
|
||||
raise ValueError("MCPClient config must include 'command' or 'url'")
|
||||
|
||||
self.session = await stack.enter_async_context(
|
||||
ClientSession(
|
||||
read_stream=read_stream,
|
||||
write_stream=write_stream,
|
||||
read_timeout_seconds=timedelta(seconds=15),
|
||||
)
|
||||
)
|
||||
await self.session.initialize()
|
||||
|
||||
# 获取工具
|
||||
self.tools = (await self.session.list_tools()).tools
|
||||
|
||||
self._ready_evt.set()
|
||||
|
||||
# 挂起等待关闭
|
||||
await self._shutdown_evt.wait()
|
||||
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"worker error: {e}")
|
||||
self._ready_evt.set()
|
||||
raise
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
"""MCP服务管理器"""
|
||||
|
||||
import asyncio
|
||||
import os, json
|
||||
from typing import Dict, Any, List
|
||||
from .MCPClient import MCPClient
|
||||
from config.logger import setup_logging
|
||||
from plugins_func.register import register_function, ToolType
|
||||
from config.config_loader import get_project_dir
|
||||
|
||||
@@ -18,11 +18,10 @@ class MCPManager:
|
||||
初始化MCP管理器
|
||||
"""
|
||||
self.conn = conn
|
||||
self.logger = setup_logging()
|
||||
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(
|
||||
self.conn.logger.bind(tag=TAG).warning(
|
||||
f"请检查mcp服务配置文件:data/.mcp_server_settings.json"
|
||||
)
|
||||
self.client: Dict[str, MCPClient] = {}
|
||||
@@ -41,7 +40,7 @@ class MCPManager:
|
||||
config = json.load(f)
|
||||
return config.get("mcpServers", {})
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
self.conn.logger.bind(tag=TAG).error(
|
||||
f"Error loading MCP config from {self.config_path}: {e}"
|
||||
)
|
||||
return {}
|
||||
@@ -50,9 +49,9 @@ class MCPManager:
|
||||
"""初始化所有MCP服务"""
|
||||
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"
|
||||
if not srv_config.get("command") and not srv_config.get("url"):
|
||||
self.conn.logger.bind(tag=TAG).warning(
|
||||
f"Skipping server {name}: neither command nor url specified"
|
||||
)
|
||||
continue
|
||||
|
||||
@@ -60,7 +59,7 @@ class MCPManager:
|
||||
client = MCPClient(srv_config)
|
||||
await client.initialize()
|
||||
self.client[name] = client
|
||||
self.logger.bind(tag=TAG).info(f"Initialized MCP client: {name}")
|
||||
self.conn.logger.bind(tag=TAG).info(f"Initialized MCP client: {name}")
|
||||
client_tools = client.get_available_tools()
|
||||
self.tools.extend(client_tools)
|
||||
for tool in client_tools:
|
||||
@@ -73,7 +72,7 @@ class MCPManager:
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(
|
||||
self.conn.logger.bind(tag=TAG).error(
|
||||
f"Failed to initialize MCP server {name}: {e}"
|
||||
)
|
||||
self.conn.func_handler.upload_functions_desc()
|
||||
@@ -110,7 +109,7 @@ class MCPManager:
|
||||
Raises:
|
||||
ValueError: 工具未找到时抛出
|
||||
"""
|
||||
self.logger.bind(tag=TAG).info(
|
||||
self.conn.logger.bind(tag=TAG).info(
|
||||
f"Executing tool {tool_name} with arguments: {arguments}"
|
||||
)
|
||||
for client in self.client.values():
|
||||
@@ -120,12 +119,13 @@ class MCPManager:
|
||||
raise ValueError(f"Tool {tool_name} not found in any MCP server")
|
||||
|
||||
async def cleanup_all(self) -> None:
|
||||
for name, client in self.client.items():
|
||||
"""依次关闭所有 MCPClient,不让异常阻断整体流程。"""
|
||||
for name, client in list(self.client.items()):
|
||||
try:
|
||||
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}"
|
||||
await asyncio.wait_for(client.cleanup(), timeout=20)
|
||||
self.conn.logger.bind(tag=TAG).info(f"MCP client closed: {name}")
|
||||
except (asyncio.TimeoutError, Exception) as e:
|
||||
self.conn.logger.bind(tag=TAG).error(
|
||||
f"Error closing MCP client {name}: {e}"
|
||||
)
|
||||
self.client.clear()
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
import asyncio
|
||||
from config.logger import setup_logging
|
||||
import os
|
||||
import numpy as np
|
||||
import opuslib_next
|
||||
from pydub import AudioSegment
|
||||
from abc import ABC, abstractmethod
|
||||
from core.utils.tts import MarkdownCleaner
|
||||
from core.utils.util import audio_to_data
|
||||
|
||||
TAG = __name__
|
||||
logger = setup_logging()
|
||||
@@ -29,7 +27,9 @@ class TTSProviderBase(ABC):
|
||||
try:
|
||||
asyncio.run(self.text_to_speak(text, tmp_file))
|
||||
except Exception as e:
|
||||
logger.bind(tag=TAG).warning(f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}")
|
||||
logger.bind(tag=TAG).warning(
|
||||
f"语音生成失败{5 - max_repeat_time + 1}次: {text},错误: {e}"
|
||||
)
|
||||
# 未执行成功,删除文件
|
||||
if os.path.exists(tmp_file):
|
||||
os.remove(tmp_file)
|
||||
@@ -55,11 +55,11 @@ class TTSProviderBase(ABC):
|
||||
|
||||
def audio_to_pcm_data(self, audio_file_path):
|
||||
"""音频文件转换为PCM编码"""
|
||||
return self.audio_to_data(audio_file_path, is_opus=False)
|
||||
return audio_to_data(audio_file_path, is_opus=False)
|
||||
|
||||
def audio_to_opus_data(self, audio_file_path):
|
||||
"""音频文件转换为Opus编码"""
|
||||
return self.audio_to_data(audio_file_path, is_opus=True)
|
||||
return audio_to_data(audio_file_path, is_opus=True)
|
||||
|
||||
def audio_to_data(self, audio_file_path, is_opus=True):
|
||||
# 获取文件后缀名
|
||||
|
||||
@@ -4,7 +4,14 @@ from datetime import datetime
|
||||
|
||||
|
||||
class Message:
|
||||
def __init__(self, role: str, content: str = None, uniq_id: str = None, tool_calls = None, tool_call_id=None):
|
||||
def __init__(
|
||||
self,
|
||||
role: str,
|
||||
content: str = None,
|
||||
uniq_id: str = None,
|
||||
tool_calls=None,
|
||||
tool_call_id=None,
|
||||
):
|
||||
self.uniq_id = uniq_id if uniq_id is not None else str(uuid.uuid4())
|
||||
self.role = role
|
||||
self.content = content
|
||||
@@ -16,7 +23,7 @@ class Dialogue:
|
||||
def __init__(self):
|
||||
self.dialogue: List[Message] = []
|
||||
# 获取当前时间
|
||||
self.current_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
|
||||
self.current_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def put(self, message: Message):
|
||||
self.dialogue.append(message)
|
||||
@@ -25,7 +32,9 @@ class Dialogue:
|
||||
if m.tool_calls is not None:
|
||||
dialogue.append({"role": m.role, "tool_calls": m.tool_calls})
|
||||
elif m.role == "tool":
|
||||
dialogue.append({"role": m.role, "tool_call_id": m.tool_call_id, "content": m.content})
|
||||
dialogue.append(
|
||||
{"role": m.role, "tool_call_id": m.tool_call_id, "content": m.content}
|
||||
)
|
||||
else:
|
||||
dialogue.append({"role": m.role, "content": m.content})
|
||||
|
||||
@@ -44,23 +53,23 @@ class Dialogue:
|
||||
else:
|
||||
self.put(Message(role="system", content=new_content))
|
||||
|
||||
def get_llm_dialogue_with_memory(self, memory_str: str = None) -> List[Dict[str, str]]:
|
||||
def get_llm_dialogue_with_memory(
|
||||
self, memory_str: str = None
|
||||
) -> List[Dict[str, str]]:
|
||||
if memory_str is None or len(memory_str) == 0:
|
||||
return self.get_llm_dialogue()
|
||||
|
||||
|
||||
# 构建带记忆的对话
|
||||
dialogue = []
|
||||
|
||||
|
||||
# 添加系统提示和记忆
|
||||
system_message = next(
|
||||
(msg for msg in self.dialogue if msg.role == "system"), None
|
||||
)
|
||||
|
||||
|
||||
if system_message:
|
||||
enhanced_system_prompt = (
|
||||
f"{system_message.content}\n\n"
|
||||
f"相关记忆:\n{memory_str}"
|
||||
f"{system_message.content}\n\n" f"相关记忆:\n{memory_str}"
|
||||
)
|
||||
dialogue.append({"role": "system", "content": enhanced_system_prompt})
|
||||
|
||||
|
||||
@@ -2,35 +2,40 @@ import json
|
||||
import socket
|
||||
import subprocess
|
||||
import re
|
||||
import os
|
||||
import numpy as np
|
||||
import requests
|
||||
import opuslib_next
|
||||
from pydub import AudioSegment
|
||||
from typing import Dict, Any
|
||||
from core.utils import tts, llm, intent, memory, vad, asr
|
||||
|
||||
TAG = __name__
|
||||
emoji_map = {
|
||||
'neutral': '😶',
|
||||
'happy': '🙂',
|
||||
'laughing': '😆',
|
||||
'funny': '😂',
|
||||
'sad': '😔',
|
||||
'angry': '😠',
|
||||
'crying': '😭',
|
||||
'loving': '😍',
|
||||
'embarrassed': '😳',
|
||||
'surprised': '😲',
|
||||
'shocked': '😱',
|
||||
'thinking': '🤔',
|
||||
'winking': '😉',
|
||||
'cool': '😎',
|
||||
'relaxed': '😌',
|
||||
'delicious': '🤤',
|
||||
'kissy': '😘',
|
||||
'confident': '😏',
|
||||
'sleepy': '😴',
|
||||
'silly': '😜',
|
||||
'confused': '🙄'
|
||||
"neutral": "😶",
|
||||
"happy": "🙂",
|
||||
"laughing": "😆",
|
||||
"funny": "😂",
|
||||
"sad": "😔",
|
||||
"angry": "😠",
|
||||
"crying": "😭",
|
||||
"loving": "😍",
|
||||
"embarrassed": "😳",
|
||||
"surprised": "😲",
|
||||
"shocked": "😱",
|
||||
"thinking": "🤔",
|
||||
"winking": "😉",
|
||||
"cool": "😎",
|
||||
"relaxed": "😌",
|
||||
"delicious": "🤤",
|
||||
"kissy": "😘",
|
||||
"confident": "😏",
|
||||
"sleepy": "😴",
|
||||
"silly": "😜",
|
||||
"confused": "🙄",
|
||||
}
|
||||
|
||||
|
||||
def get_local_ip():
|
||||
try:
|
||||
s = socket.socket(socket.AF_INET, socket.SOCK_DGRAM)
|
||||
@@ -117,9 +122,9 @@ def is_punctuation_or_emoji(char):
|
||||
"、", # 中文顿号
|
||||
"“",
|
||||
"”",
|
||||
"\"", # 中文双引号 + 英文引号
|
||||
'"', # 中文双引号 + 英文引号
|
||||
":",
|
||||
":", # 中文冒号 + 英文冒号
|
||||
":", # 中文冒号 + 英文冒号
|
||||
}
|
||||
if char.isspace() or char in punctuation_set:
|
||||
return True
|
||||
@@ -345,20 +350,15 @@ def initialize_modules(
|
||||
str(config.get("delete_audio", True)).lower() in ("true", "1", "yes"),
|
||||
)
|
||||
logger.bind(tag=TAG).info(f"初始化组件: asr成功 {select_asr_module}")
|
||||
|
||||
# 初始化自定义prompt
|
||||
if config.get("prompt", None) is not None:
|
||||
modules["prompt"] = config["prompt"]
|
||||
logger.bind(tag=TAG).info(f"初始化组件: prompt成功 {modules['prompt'][:50]}...")
|
||||
|
||||
return modules
|
||||
|
||||
|
||||
def analyze_emotion(text):
|
||||
"""
|
||||
分析文本情感并返回对应的emoji名称(支持中英文)
|
||||
"""
|
||||
if not text or not isinstance(text, str):
|
||||
return 'neutral'
|
||||
return "neutral"
|
||||
|
||||
original_text = text
|
||||
text = text.lower().strip()
|
||||
@@ -369,84 +369,444 @@ def analyze_emotion(text):
|
||||
return emotion
|
||||
|
||||
# 标点符号分析
|
||||
has_exclamation = '!' in original_text or '!' in original_text
|
||||
has_question = '?' in original_text or '?' in original_text
|
||||
has_ellipsis = '...' in original_text or '…' in original_text
|
||||
has_exclamation = "!" in original_text or "!" in original_text
|
||||
has_question = "?" in original_text or "?" in original_text
|
||||
has_ellipsis = "..." in original_text or "…" in original_text
|
||||
|
||||
# 定义情感关键词映射(中英文扩展版)
|
||||
emotion_keywords = {
|
||||
'happy': ['开心', '高兴', '快乐', '愉快', '幸福', '满意', '棒', '好', '不错', '完美', '棒极了', '太好了',
|
||||
'好呀', '好的', 'happy', 'joy', 'great', 'good', 'nice', 'awesome', 'fantastic', 'wonderful'],
|
||||
'laughing': ['哈哈', '哈哈哈', '呵呵', '嘿嘿', '嘻嘻', '笑死', '太好笑了', '笑死我了', 'lol', 'lmao', 'haha',
|
||||
'hahaha', 'hehe', 'rofl', 'funny', 'laugh'],
|
||||
'funny': ['搞笑', '滑稽', '逗', '幽默', '笑点', '段子', '笑话', '太逗了', 'hilarious', 'joke', 'comedy'],
|
||||
'sad': ['伤心', '难过', '悲哀', '悲伤', '忧郁', '郁闷', '沮丧', '失望', '想哭', '难受', '不开心', '唉', '呜呜',
|
||||
'sad', 'upset', 'unhappy', 'depressed', 'sorrow', 'gloomy'],
|
||||
'angry': ['生气', '愤怒', '气死', '讨厌', '烦人', '可恶', '烦死了', '恼火', '暴躁', '火大', '愤怒', '气炸了',
|
||||
'angry', 'mad', 'annoyed', 'furious', 'pissed', 'hate'],
|
||||
'crying': ['哭泣', '泪流', '大哭', '伤心欲绝', '泪目', '流泪', '哭死', '哭晕', '想哭', '泪崩',
|
||||
'cry', 'crying', 'tears', 'sob', 'weep'],
|
||||
'loving': ['爱你', '喜欢', '爱', '亲爱的', '宝贝', '么么哒', '抱抱', '想你', '思念', '最爱', '亲亲', '喜欢你',
|
||||
'love', 'like', 'adore', 'darling', 'sweetie', 'honey', 'miss you', 'heart'],
|
||||
'embarrassed': ['尴尬', '不好意思', '害羞', '脸红', '难为情', '社死', '丢脸', '出丑',
|
||||
'embarrassed', 'awkward', 'shy', 'blush'],
|
||||
'surprised': ['惊讶', '吃惊', '天啊', '哇塞', '哇', '居然', '竟然', '没想到', '出乎意料',
|
||||
'surprise', 'wow', 'omg', 'oh my god', 'amazing', 'unbelievable'],
|
||||
'shocked': ['震惊', '吓到', '惊呆了', '不敢相信', '震撼', '吓死', '恐怖', '害怕', '吓人',
|
||||
'shocked', 'shocking', 'scared', 'frightened', 'terrified', 'horror'],
|
||||
'thinking': ['思考', '考虑', '想一下', '琢磨', '沉思', '冥想', '想', '思考中', '在想',
|
||||
'think', 'thinking', 'consider', 'ponder', 'meditate'],
|
||||
'winking': ['调皮', '眨眼', '你懂的', '坏笑', '邪恶', '奸笑', '使眼色',
|
||||
'wink', 'teasing', 'naughty', 'mischievous'],
|
||||
'cool': ['酷', '帅', '厉害', '棒极了', '真棒', '牛逼', '强', '优秀', '杰出', '出色', '完美',
|
||||
'cool', 'awesome', 'amazing', 'great', 'impressive', 'perfect'],
|
||||
'relaxed': ['放松', '舒服', '惬意', '悠闲', '轻松', '舒适', '安逸', '自在',
|
||||
'relax', 'relaxed', 'comfortable', 'cozy', 'chill', 'peaceful'],
|
||||
'delicious': ['好吃', '美味', '香', '馋', '可口', '香甜', '大餐', '大快朵颐', '流口水', '垂涎',
|
||||
'delicious', 'yummy', 'tasty', 'yum', 'appetizing', 'mouthwatering'],
|
||||
'kissy': ['亲亲', '么么', '吻', 'mua', 'muah', '亲一下', '飞吻',
|
||||
'kiss', 'xoxo', 'hug', 'muah', 'smooch'],
|
||||
'confident': ['自信', '肯定', '确定', '毫无疑问', '当然', '必须的', '毫无疑问', '确信', '坚信',
|
||||
'confident', 'sure', 'certain', 'definitely', 'positive'],
|
||||
'sleepy': ['困', '睡觉', '晚安', '想睡', '好累', '疲惫', '疲倦', '困了', '想休息', '睡意',
|
||||
'sleep', 'sleepy', 'tired', 'exhausted', 'bedtime', 'good night'],
|
||||
'silly': ['傻', '笨', '呆', '憨', '蠢', '二', '憨憨', '傻乎乎', '呆萌',
|
||||
'silly', 'stupid', 'dumb', 'foolish', 'goofy', 'ridiculous'],
|
||||
'confused': ['疑惑', '不明白', '不懂', '困惑', '疑问', '为什么', '怎么回事', '啥意思', '不清楚',
|
||||
'confused', 'puzzled', 'doubt', 'question', 'what', 'why', 'how']
|
||||
"happy": [
|
||||
"开心",
|
||||
"高兴",
|
||||
"快乐",
|
||||
"愉快",
|
||||
"幸福",
|
||||
"满意",
|
||||
"棒",
|
||||
"好",
|
||||
"不错",
|
||||
"完美",
|
||||
"棒极了",
|
||||
"太好了",
|
||||
"好呀",
|
||||
"好的",
|
||||
"happy",
|
||||
"joy",
|
||||
"great",
|
||||
"good",
|
||||
"nice",
|
||||
"awesome",
|
||||
"fantastic",
|
||||
"wonderful",
|
||||
],
|
||||
"laughing": [
|
||||
"哈哈",
|
||||
"哈哈哈",
|
||||
"呵呵",
|
||||
"嘿嘿",
|
||||
"嘻嘻",
|
||||
"笑死",
|
||||
"太好笑了",
|
||||
"笑死我了",
|
||||
"lol",
|
||||
"lmao",
|
||||
"haha",
|
||||
"hahaha",
|
||||
"hehe",
|
||||
"rofl",
|
||||
"funny",
|
||||
"laugh",
|
||||
],
|
||||
"funny": [
|
||||
"搞笑",
|
||||
"滑稽",
|
||||
"逗",
|
||||
"幽默",
|
||||
"笑点",
|
||||
"段子",
|
||||
"笑话",
|
||||
"太逗了",
|
||||
"hilarious",
|
||||
"joke",
|
||||
"comedy",
|
||||
],
|
||||
"sad": [
|
||||
"伤心",
|
||||
"难过",
|
||||
"悲哀",
|
||||
"悲伤",
|
||||
"忧郁",
|
||||
"郁闷",
|
||||
"沮丧",
|
||||
"失望",
|
||||
"想哭",
|
||||
"难受",
|
||||
"不开心",
|
||||
"唉",
|
||||
"呜呜",
|
||||
"sad",
|
||||
"upset",
|
||||
"unhappy",
|
||||
"depressed",
|
||||
"sorrow",
|
||||
"gloomy",
|
||||
],
|
||||
"angry": [
|
||||
"生气",
|
||||
"愤怒",
|
||||
"气死",
|
||||
"讨厌",
|
||||
"烦人",
|
||||
"可恶",
|
||||
"烦死了",
|
||||
"恼火",
|
||||
"暴躁",
|
||||
"火大",
|
||||
"愤怒",
|
||||
"气炸了",
|
||||
"angry",
|
||||
"mad",
|
||||
"annoyed",
|
||||
"furious",
|
||||
"pissed",
|
||||
"hate",
|
||||
],
|
||||
"crying": [
|
||||
"哭泣",
|
||||
"泪流",
|
||||
"大哭",
|
||||
"伤心欲绝",
|
||||
"泪目",
|
||||
"流泪",
|
||||
"哭死",
|
||||
"哭晕",
|
||||
"想哭",
|
||||
"泪崩",
|
||||
"cry",
|
||||
"crying",
|
||||
"tears",
|
||||
"sob",
|
||||
"weep",
|
||||
],
|
||||
"loving": [
|
||||
"爱你",
|
||||
"喜欢",
|
||||
"爱",
|
||||
"亲爱的",
|
||||
"宝贝",
|
||||
"么么哒",
|
||||
"抱抱",
|
||||
"想你",
|
||||
"思念",
|
||||
"最爱",
|
||||
"亲亲",
|
||||
"喜欢你",
|
||||
"love",
|
||||
"like",
|
||||
"adore",
|
||||
"darling",
|
||||
"sweetie",
|
||||
"honey",
|
||||
"miss you",
|
||||
"heart",
|
||||
],
|
||||
"embarrassed": [
|
||||
"尴尬",
|
||||
"不好意思",
|
||||
"害羞",
|
||||
"脸红",
|
||||
"难为情",
|
||||
"社死",
|
||||
"丢脸",
|
||||
"出丑",
|
||||
"embarrassed",
|
||||
"awkward",
|
||||
"shy",
|
||||
"blush",
|
||||
],
|
||||
"surprised": [
|
||||
"惊讶",
|
||||
"吃惊",
|
||||
"天啊",
|
||||
"哇塞",
|
||||
"哇",
|
||||
"居然",
|
||||
"竟然",
|
||||
"没想到",
|
||||
"出乎意料",
|
||||
"surprise",
|
||||
"wow",
|
||||
"omg",
|
||||
"oh my god",
|
||||
"amazing",
|
||||
"unbelievable",
|
||||
],
|
||||
"shocked": [
|
||||
"震惊",
|
||||
"吓到",
|
||||
"惊呆了",
|
||||
"不敢相信",
|
||||
"震撼",
|
||||
"吓死",
|
||||
"恐怖",
|
||||
"害怕",
|
||||
"吓人",
|
||||
"shocked",
|
||||
"shocking",
|
||||
"scared",
|
||||
"frightened",
|
||||
"terrified",
|
||||
"horror",
|
||||
],
|
||||
"thinking": [
|
||||
"思考",
|
||||
"考虑",
|
||||
"想一下",
|
||||
"琢磨",
|
||||
"沉思",
|
||||
"冥想",
|
||||
"想",
|
||||
"思考中",
|
||||
"在想",
|
||||
"think",
|
||||
"thinking",
|
||||
"consider",
|
||||
"ponder",
|
||||
"meditate",
|
||||
],
|
||||
"winking": [
|
||||
"调皮",
|
||||
"眨眼",
|
||||
"你懂的",
|
||||
"坏笑",
|
||||
"邪恶",
|
||||
"奸笑",
|
||||
"使眼色",
|
||||
"wink",
|
||||
"teasing",
|
||||
"naughty",
|
||||
"mischievous",
|
||||
],
|
||||
"cool": [
|
||||
"酷",
|
||||
"帅",
|
||||
"厉害",
|
||||
"棒极了",
|
||||
"真棒",
|
||||
"牛逼",
|
||||
"强",
|
||||
"优秀",
|
||||
"杰出",
|
||||
"出色",
|
||||
"完美",
|
||||
"cool",
|
||||
"awesome",
|
||||
"amazing",
|
||||
"great",
|
||||
"impressive",
|
||||
"perfect",
|
||||
],
|
||||
"relaxed": [
|
||||
"放松",
|
||||
"舒服",
|
||||
"惬意",
|
||||
"悠闲",
|
||||
"轻松",
|
||||
"舒适",
|
||||
"安逸",
|
||||
"自在",
|
||||
"relax",
|
||||
"relaxed",
|
||||
"comfortable",
|
||||
"cozy",
|
||||
"chill",
|
||||
"peaceful",
|
||||
],
|
||||
"delicious": [
|
||||
"好吃",
|
||||
"美味",
|
||||
"香",
|
||||
"馋",
|
||||
"可口",
|
||||
"香甜",
|
||||
"大餐",
|
||||
"大快朵颐",
|
||||
"流口水",
|
||||
"垂涎",
|
||||
"delicious",
|
||||
"yummy",
|
||||
"tasty",
|
||||
"yum",
|
||||
"appetizing",
|
||||
"mouthwatering",
|
||||
],
|
||||
"kissy": [
|
||||
"亲亲",
|
||||
"么么",
|
||||
"吻",
|
||||
"mua",
|
||||
"muah",
|
||||
"亲一下",
|
||||
"飞吻",
|
||||
"kiss",
|
||||
"xoxo",
|
||||
"hug",
|
||||
"muah",
|
||||
"smooch",
|
||||
],
|
||||
"confident": [
|
||||
"自信",
|
||||
"肯定",
|
||||
"确定",
|
||||
"毫无疑问",
|
||||
"当然",
|
||||
"必须的",
|
||||
"毫无疑问",
|
||||
"确信",
|
||||
"坚信",
|
||||
"confident",
|
||||
"sure",
|
||||
"certain",
|
||||
"definitely",
|
||||
"positive",
|
||||
],
|
||||
"sleepy": [
|
||||
"困",
|
||||
"睡觉",
|
||||
"晚安",
|
||||
"想睡",
|
||||
"好累",
|
||||
"疲惫",
|
||||
"疲倦",
|
||||
"困了",
|
||||
"想休息",
|
||||
"睡意",
|
||||
"sleep",
|
||||
"sleepy",
|
||||
"tired",
|
||||
"exhausted",
|
||||
"bedtime",
|
||||
"good night",
|
||||
],
|
||||
"silly": [
|
||||
"傻",
|
||||
"笨",
|
||||
"呆",
|
||||
"憨",
|
||||
"蠢",
|
||||
"二",
|
||||
"憨憨",
|
||||
"傻乎乎",
|
||||
"呆萌",
|
||||
"silly",
|
||||
"stupid",
|
||||
"dumb",
|
||||
"foolish",
|
||||
"goofy",
|
||||
"ridiculous",
|
||||
],
|
||||
"confused": [
|
||||
"疑惑",
|
||||
"不明白",
|
||||
"不懂",
|
||||
"困惑",
|
||||
"疑问",
|
||||
"为什么",
|
||||
"怎么回事",
|
||||
"啥意思",
|
||||
"不清楚",
|
||||
"confused",
|
||||
"puzzled",
|
||||
"doubt",
|
||||
"question",
|
||||
"what",
|
||||
"why",
|
||||
"how",
|
||||
],
|
||||
}
|
||||
|
||||
# 特殊句型判断(中英文)
|
||||
# 赞美他人
|
||||
if any(phrase in text for phrase in
|
||||
['你真', '你好', '您真', '你真棒', '你好厉害', '你太强了', '你真好', '你真聪明',
|
||||
'you are', 'you\'re', 'you look', 'you seem', 'so smart', 'so kind']):
|
||||
return 'loving'
|
||||
if any(
|
||||
phrase in text
|
||||
for phrase in [
|
||||
"你真",
|
||||
"你好",
|
||||
"您真",
|
||||
"你真棒",
|
||||
"你好厉害",
|
||||
"你太强了",
|
||||
"你真好",
|
||||
"你真聪明",
|
||||
"you are",
|
||||
"you're",
|
||||
"you look",
|
||||
"you seem",
|
||||
"so smart",
|
||||
"so kind",
|
||||
]
|
||||
):
|
||||
return "loving"
|
||||
# 自我赞美
|
||||
if any(phrase in text for phrase in ['我真', '我最', '我太棒了', '我厉害', '我聪明', '我优秀',
|
||||
'i am', 'i\'m', 'i feel', 'so good', 'so happy']):
|
||||
return 'cool'
|
||||
if any(
|
||||
phrase in text
|
||||
for phrase in [
|
||||
"我真",
|
||||
"我最",
|
||||
"我太棒了",
|
||||
"我厉害",
|
||||
"我聪明",
|
||||
"我优秀",
|
||||
"i am",
|
||||
"i'm",
|
||||
"i feel",
|
||||
"so good",
|
||||
"so happy",
|
||||
]
|
||||
):
|
||||
return "cool"
|
||||
# 晚安/睡觉相关
|
||||
if any(phrase in text for phrase in ['睡觉', '晚安', '睡了', '好梦', '休息了', '去睡了',
|
||||
'sleep', 'good night', 'bedtime', 'go to bed']):
|
||||
return 'sleepy'
|
||||
if any(
|
||||
phrase in text
|
||||
for phrase in [
|
||||
"睡觉",
|
||||
"晚安",
|
||||
"睡了",
|
||||
"好梦",
|
||||
"休息了",
|
||||
"去睡了",
|
||||
"sleep",
|
||||
"good night",
|
||||
"bedtime",
|
||||
"go to bed",
|
||||
]
|
||||
):
|
||||
return "sleepy"
|
||||
# 疑问句
|
||||
if has_question and not has_exclamation:
|
||||
return 'thinking'
|
||||
return "thinking"
|
||||
# 强烈情感(感叹号)
|
||||
if has_exclamation and not has_question:
|
||||
# 检查是否是积极内容
|
||||
positive_words = emotion_keywords['happy'] + emotion_keywords['laughing'] + emotion_keywords['cool']
|
||||
positive_words = (
|
||||
emotion_keywords["happy"]
|
||||
+ emotion_keywords["laughing"]
|
||||
+ emotion_keywords["cool"]
|
||||
)
|
||||
if any(word in text for word in positive_words):
|
||||
return 'laughing'
|
||||
return "laughing"
|
||||
# 检查是否是消极内容
|
||||
negative_words = emotion_keywords['angry'] + emotion_keywords['sad'] + emotion_keywords['crying']
|
||||
negative_words = (
|
||||
emotion_keywords["angry"]
|
||||
+ emotion_keywords["sad"]
|
||||
+ emotion_keywords["crying"]
|
||||
)
|
||||
if any(word in text for word in negative_words):
|
||||
return 'angry'
|
||||
return 'surprised'
|
||||
return "angry"
|
||||
return "surprised"
|
||||
# 省略号(表示犹豫或思考)
|
||||
if has_ellipsis:
|
||||
return 'thinking'
|
||||
return "thinking"
|
||||
|
||||
# 关键词匹配(带权重)
|
||||
emotion_scores = {emotion: 0 for emotion in emoji_map.keys()}
|
||||
@@ -466,18 +826,33 @@ def analyze_emotion(text):
|
||||
# 根据分数选择最可能的情感
|
||||
max_score = max(emotion_scores.values())
|
||||
if max_score == 0:
|
||||
return 'happy' # 默认
|
||||
return "happy" # 默认
|
||||
|
||||
# 可能有多个情感同分,根据上下文选择最合适的
|
||||
top_emotions = [e for e, s in emotion_scores.items() if s == max_score]
|
||||
|
||||
# 如果多个情感同分,使用以下优先级
|
||||
priority_order = [
|
||||
'laughing', 'crying', 'angry', 'surprised', 'shocked', # 强烈情感优先
|
||||
'loving', 'happy', 'funny', 'cool', # 积极情感
|
||||
'sad', 'embarrassed', 'confused', # 消极情感
|
||||
'thinking', 'winking', 'relaxed', # 中性情感
|
||||
'delicious', 'kissy', 'confident', 'sleepy', 'silly' # 特殊场景
|
||||
"laughing",
|
||||
"crying",
|
||||
"angry",
|
||||
"surprised",
|
||||
"shocked", # 强烈情感优先
|
||||
"loving",
|
||||
"happy",
|
||||
"funny",
|
||||
"cool", # 积极情感
|
||||
"sad",
|
||||
"embarrassed",
|
||||
"confused", # 消极情感
|
||||
"thinking",
|
||||
"winking",
|
||||
"relaxed", # 中性情感
|
||||
"delicious",
|
||||
"kissy",
|
||||
"confident",
|
||||
"sleepy",
|
||||
"silly", # 特殊场景
|
||||
]
|
||||
|
||||
for emotion in priority_order:
|
||||
@@ -485,3 +860,98 @@ def analyze_emotion(text):
|
||||
return emotion
|
||||
|
||||
return top_emotions[0] # 如果都不在优先级列表里,返回第一个
|
||||
|
||||
|
||||
def audio_to_opus_data(audio_file_path):
|
||||
"""音频文件转换为Opus编码"""
|
||||
# 获取文件后缀名
|
||||
file_type = os.path.splitext(audio_file_path)[1]
|
||||
if file_type:
|
||||
file_type = file_type.lstrip(".")
|
||||
# 读取音频文件,-nostdin 参数:不要从标准输入读取数据,否则FFmpeg会阻塞
|
||||
audio = AudioSegment.from_file(
|
||||
audio_file_path, format=file_type, parameters=["-nostdin"]
|
||||
)
|
||||
|
||||
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
|
||||
audio = audio.set_channels(1).set_frame_rate(16000).set_sample_width(2)
|
||||
|
||||
# 音频时长(秒)
|
||||
duration = len(audio) / 1000.0
|
||||
|
||||
# 获取原始PCM数据(16位小端)
|
||||
raw_data = audio.raw_data
|
||||
|
||||
# 初始化Opus编码器
|
||||
encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO)
|
||||
|
||||
# 编码参数
|
||||
frame_duration = 60 # 60ms per frame
|
||||
frame_size = int(16000 * frame_duration / 1000) # 960 samples/frame
|
||||
|
||||
opus_datas = []
|
||||
# 按帧处理所有音频数据(包括最后一帧可能补零)
|
||||
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
|
||||
# 获取当前帧的二进制数据
|
||||
chunk = raw_data[i : i + frame_size * 2]
|
||||
|
||||
# 如果最后一帧不足,补零
|
||||
if len(chunk) < frame_size * 2:
|
||||
chunk += b"\x00" * (frame_size * 2 - len(chunk))
|
||||
|
||||
# 转换为numpy数组处理
|
||||
np_frame = np.frombuffer(chunk, dtype=np.int16)
|
||||
|
||||
# 编码Opus数据
|
||||
opus_data = encoder.encode(np_frame.tobytes(), frame_size)
|
||||
opus_datas.append(opus_data)
|
||||
|
||||
return opus_datas, duration
|
||||
|
||||
|
||||
def check_vad_update(before_config, new_config):
|
||||
if (
|
||||
new_config.get("selected_module") is None
|
||||
or new_config["selected_module"].get("VAD") is None
|
||||
):
|
||||
return False
|
||||
update_vad = False
|
||||
current_vad_module = before_config["selected_module"]["VAD"]
|
||||
new_vad_module = new_config["selected_module"]["VAD"]
|
||||
current_vad_type = (
|
||||
current_vad_module
|
||||
if "type" not in before_config["VAD"][current_vad_module]
|
||||
else before_config["VAD"][current_vad_module]["type"]
|
||||
)
|
||||
new_vad_type = (
|
||||
new_vad_module
|
||||
if "type" not in new_config["VAD"][new_vad_module]
|
||||
else new_config["VAD"][new_vad_module]["type"]
|
||||
)
|
||||
print(f"前vad:{current_vad_type},后vad:{new_vad_type}")
|
||||
update_vad = current_vad_type != new_vad_type
|
||||
return update_vad
|
||||
|
||||
|
||||
def check_asr_update(before_config, new_config):
|
||||
if (
|
||||
new_config.get("selected_module") is None
|
||||
or new_config["selected_module"].get("ASR") is None
|
||||
):
|
||||
return False
|
||||
update_asr = False
|
||||
current_asr_module = before_config["selected_module"]["ASR"]
|
||||
new_asr_module = new_config["selected_module"]["ASR"]
|
||||
current_asr_type = (
|
||||
current_asr_module
|
||||
if "type" not in before_config["ASR"][current_asr_module]
|
||||
else before_config["ASR"][current_asr_module]["type"]
|
||||
)
|
||||
new_asr_type = (
|
||||
new_asr_module
|
||||
if "type" not in new_config["ASR"][new_asr_module]
|
||||
else new_config["ASR"][new_asr_module]["type"]
|
||||
)
|
||||
print(f"前asr:{current_asr_type},后asr:{new_asr_type}")
|
||||
update_asr = current_asr_type != new_asr_type
|
||||
return update_asr
|
||||
|
||||
@@ -2,7 +2,8 @@ import asyncio
|
||||
import websockets
|
||||
from config.logger import setup_logging
|
||||
from core.connection import ConnectionHandler
|
||||
from core.utils.util import initialize_modules
|
||||
from core.utils.util import initialize_modules, check_vad_update, check_asr_update
|
||||
from config.config_loader import get_config_from_api
|
||||
|
||||
TAG = __name__
|
||||
|
||||
@@ -13,14 +14,21 @@ class WebSocketServer:
|
||||
self.logger = setup_logging()
|
||||
self.config_lock = asyncio.Lock()
|
||||
modules = initialize_modules(
|
||||
self.logger, self.config, True, True, True, True, True, True
|
||||
self.logger,
|
||||
self.config,
|
||||
"VAD" in self.config["selected_module"],
|
||||
"ASR" in self.config["selected_module"],
|
||||
"LLM" in self.config["selected_module"],
|
||||
"TTS" in self.config["selected_module"],
|
||||
"Memory" in self.config["selected_module"],
|
||||
"Intent" in self.config["selected_module"],
|
||||
)
|
||||
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._vad = modules["vad"] if "vad" in modules else None
|
||||
self._asr = modules["asr"] if "asr" in modules else None
|
||||
self._tts = modules["tts"] if "tts" in modules else None
|
||||
self._llm = modules["llm"] if "llm" in modules else None
|
||||
self._intent = modules["intent"] if "intent" in modules else None
|
||||
self._memory = modules["memory"] if "memory" in modules else None
|
||||
self.active_connections = set()
|
||||
|
||||
async def start(self):
|
||||
@@ -44,7 +52,7 @@ class WebSocketServer:
|
||||
self._tts,
|
||||
self._memory,
|
||||
self._intent,
|
||||
self # 传入当前 WebSocketServer 实例
|
||||
self, # 传入server实例
|
||||
)
|
||||
self.active_connections.add(handler)
|
||||
try:
|
||||
@@ -60,3 +68,54 @@ class WebSocketServer:
|
||||
else:
|
||||
# 如果是普通 HTTP 请求,返回 "server is running"
|
||||
return websocket.respond(200, "Server is running\n")
|
||||
|
||||
async def update_config(self) -> bool:
|
||||
"""更新服务器配置并重新初始化组件
|
||||
|
||||
Returns:
|
||||
bool: 更新是否成功
|
||||
"""
|
||||
try:
|
||||
async with self.config_lock:
|
||||
# 重新获取配置
|
||||
new_config = get_config_from_api(self.config)
|
||||
if new_config is None:
|
||||
self.logger.bind(tag=TAG).error("获取新配置失败")
|
||||
return False
|
||||
|
||||
# 检查 VAD 和 ASR 类型是否需要更新
|
||||
update_vad = check_vad_update(self.config, new_config)
|
||||
update_asr = check_asr_update(self.config, new_config)
|
||||
|
||||
# 更新配置
|
||||
self.config = new_config
|
||||
# 重新初始化组件
|
||||
modules = initialize_modules(
|
||||
self.logger,
|
||||
new_config,
|
||||
update_vad,
|
||||
update_asr,
|
||||
"LLM" in new_config["selected_module"],
|
||||
"TTS" in new_config["selected_module"],
|
||||
"Memory" in new_config["selected_module"],
|
||||
"Intent" in new_config["selected_module"],
|
||||
)
|
||||
|
||||
# 更新组件实例
|
||||
if "vad" in modules:
|
||||
self._vad = modules["vad"]
|
||||
if "asr" in modules:
|
||||
self._asr = modules["asr"]
|
||||
if "tts" in modules:
|
||||
self._tts = modules["tts"]
|
||||
if "llm" in modules:
|
||||
self._llm = modules["llm"]
|
||||
if "intent" in modules:
|
||||
self._intent = modules["intent"]
|
||||
if "memory" in modules:
|
||||
self._memory = modules["memory"]
|
||||
|
||||
return True
|
||||
except Exception as e:
|
||||
self.logger.bind(tag=TAG).error(f"更新服务器配置失败: {str(e)}")
|
||||
return False
|
||||
|
||||
Reference in New Issue
Block a user