mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-27 17:43:55 +08:00
update:格式化代码
This commit is contained in:
@@ -14,7 +14,11 @@ from config.logger import setup_logging
|
|||||||
from core.providers.tts.dto.dto import TTSMessageDTO, MsgType
|
from core.providers.tts.dto.dto import TTSMessageDTO, MsgType
|
||||||
from core.utils.dialogue import Message, Dialogue
|
from core.utils.dialogue import Message, Dialogue
|
||||||
from core.handle.textHandle import handleTextMessage
|
from core.handle.textHandle import handleTextMessage
|
||||||
from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_from_string, get_ip_info
|
from core.utils.util import (
|
||||||
|
get_string_no_punctuation_or_emoji,
|
||||||
|
extract_json_from_string,
|
||||||
|
get_ip_info,
|
||||||
|
)
|
||||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||||
from core.handle.sendAudioHandle import sendAudioMessage
|
from core.handle.sendAudioHandle import sendAudioMessage
|
||||||
from core.handle.receiveAudioHandle import handleAudioMessage
|
from core.handle.receiveAudioHandle import handleAudioMessage
|
||||||
@@ -32,7 +36,9 @@ class TTSException(RuntimeError):
|
|||||||
|
|
||||||
|
|
||||||
class ConnectionHandler:
|
class ConnectionHandler:
|
||||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent):
|
def __init__(
|
||||||
|
self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _memory, _intent
|
||||||
|
):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.logger = setup_logging()
|
self.logger = setup_logging()
|
||||||
self.auth = AuthMiddleware(config)
|
self.auth = AuthMiddleware(config)
|
||||||
@@ -104,7 +110,7 @@ class ConnectionHandler:
|
|||||||
self.is_device_verified = False # 添加设备验证状态标志
|
self.is_device_verified = False # 添加设备验证状态标志
|
||||||
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
self.close_after_chat = False # 是否在聊天结束后关闭连接
|
||||||
self.use_function_call_mode = False
|
self.use_function_call_mode = False
|
||||||
if self.config["selected_module"]["Intent"] == 'function_call':
|
if self.config["selected_module"]["Intent"] == "function_call":
|
||||||
self.use_function_call_mode = True
|
self.use_function_call_mode = True
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
@@ -113,7 +119,9 @@ class ConnectionHandler:
|
|||||||
self.headers = dict(ws.request.headers)
|
self.headers = dict(ws.request.headers)
|
||||||
# 获取客户端ip地址
|
# 获取客户端ip地址
|
||||||
self.client_ip = ws.remote_address[0]
|
self.client_ip = ws.remote_address[0]
|
||||||
self.logger.bind(tag=TAG).info(f"{self.client_ip} conn - Headers: {self.headers}")
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"{self.client_ip} conn - Headers: {self.headers}"
|
||||||
|
)
|
||||||
|
|
||||||
# 进行认证
|
# 进行认证
|
||||||
await self.auth.authenticate(self.headers)
|
await self.auth.authenticate(self.headers)
|
||||||
@@ -124,10 +132,14 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
# Load private configuration if device_id is provided
|
# Load private configuration if device_id is provided
|
||||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||||
self.logger.bind(tag=TAG).info(f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}")
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}"
|
||||||
|
)
|
||||||
if bUsePrivateConfig and device_id:
|
if bUsePrivateConfig and device_id:
|
||||||
try:
|
try:
|
||||||
self.private_config = PrivateConfig(device_id, self.config, self.auth_code_gen)
|
self.private_config = PrivateConfig(
|
||||||
|
device_id, self.config, self.auth_code_gen
|
||||||
|
)
|
||||||
await self.private_config.load_or_create()
|
await self.private_config.load_or_create()
|
||||||
# 判断是否已经绑定
|
# 判断是否已经绑定
|
||||||
owner = self.private_config.get_owner()
|
owner = self.private_config.get_owner()
|
||||||
@@ -140,12 +152,18 @@ class ConnectionHandler:
|
|||||||
if all([llm, tts]):
|
if all([llm, tts]):
|
||||||
self.llm = llm
|
self.llm = llm
|
||||||
self.tts = tts
|
self.tts = tts
|
||||||
self.logger.bind(tag=TAG).info(f"Loaded private config and instances for device {device_id}")
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"Loaded private config and instances for device {device_id}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).error(f"Failed to create instances for device {device_id}")
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"Failed to create instances for device {device_id}"
|
||||||
|
)
|
||||||
self.private_config = None
|
self.private_config = None
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"Error initializing private config: {e}")
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"Error initializing private config: {e}"
|
||||||
|
)
|
||||||
self.private_config = None
|
self.private_config = None
|
||||||
raise
|
raise
|
||||||
|
|
||||||
@@ -162,7 +180,9 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
# 音频播放 消化线程
|
# 音频播放 消化线程
|
||||||
self.stop_event.clear()
|
self.stop_event.clear()
|
||||||
audio_play_priority = threading.Thread(target=self._audio_play_priority_thread, daemon=True)
|
audio_play_priority = threading.Thread(
|
||||||
|
target=self._audio_play_priority_thread, daemon=True
|
||||||
|
)
|
||||||
audio_play_priority.start()
|
audio_play_priority.start()
|
||||||
|
|
||||||
# 打开音频通道
|
# 打开音频通道
|
||||||
@@ -236,7 +256,9 @@ class ConnectionHandler:
|
|||||||
def chat(self, query):
|
def chat(self, query):
|
||||||
if self.isNeedAuth():
|
if self.isNeedAuth():
|
||||||
self.llm_finish_task = True
|
self.llm_finish_task = True
|
||||||
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self._check_and_broadcast_auth_code(), self.loop
|
||||||
|
)
|
||||||
future.result()
|
future.result()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -247,13 +269,14 @@ class ConnectionHandler:
|
|||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
# 使用带记忆的对话
|
# 使用带记忆的对话
|
||||||
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self.memory.query_memory(query), self.loop
|
||||||
|
)
|
||||||
memory_str = future.result()
|
memory_str = future.result()
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
|
self.logger.bind(tag=TAG).debug(f"记忆内容: {memory_str}")
|
||||||
llm_responses = self.llm.response(
|
llm_responses = self.llm.response(
|
||||||
self.session_id,
|
self.session_id, self.dialogue.get_llm_dialogue_with_memory(memory_str)
|
||||||
self.dialogue.get_llm_dialogue_with_memory(memory_str)
|
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
|
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
|
||||||
@@ -270,20 +293,34 @@ class ConnectionHandler:
|
|||||||
break
|
break
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
|
self.logger.bind(tag=TAG).debug(
|
||||||
|
f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}"
|
||||||
|
)
|
||||||
if text_index == 0:
|
if text_index == 0:
|
||||||
self.tts.tts_text_queue.put(
|
self.tts.tts_text_queue.put(
|
||||||
TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content=''))
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content=""
|
||||||
|
)
|
||||||
|
)
|
||||||
self.start_tts_request_flag = True
|
self.start_tts_request_flag = True
|
||||||
self.tts.tts_text_queue.put(
|
self.tts.tts_text_queue.put(
|
||||||
TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=content))
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=content
|
||||||
|
)
|
||||||
|
)
|
||||||
text_index += 1
|
text_index += 1
|
||||||
if self.start_tts_request_flag:
|
if self.start_tts_request_flag:
|
||||||
self.start_tts_request_flag = False
|
self.start_tts_request_flag = False
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=''))
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=""
|
||||||
|
)
|
||||||
|
)
|
||||||
self.llm_finish_task = True
|
self.llm_finish_task = True
|
||||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
||||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
self.logger.bind(tag=TAG).debug(
|
||||||
|
json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)
|
||||||
|
)
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def chat_with_function_calling(self, query, tool_call=False):
|
def chat_with_function_calling(self, query, tool_call=False):
|
||||||
@@ -291,7 +328,9 @@ class ConnectionHandler:
|
|||||||
"""Chat with function calling for intent detection using streaming"""
|
"""Chat with function calling for intent detection using streaming"""
|
||||||
if self.isNeedAuth():
|
if self.isNeedAuth():
|
||||||
self.llm_finish_task = True
|
self.llm_finish_task = True
|
||||||
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self._check_and_broadcast_auth_code(), self.loop
|
||||||
|
)
|
||||||
future.result()
|
future.result()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -308,7 +347,9 @@ class ConnectionHandler:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
|
|
||||||
# 使用带记忆的对话
|
# 使用带记忆的对话
|
||||||
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self.memory.query_memory(query), self.loop
|
||||||
|
)
|
||||||
memory_str = future.result()
|
memory_str = future.result()
|
||||||
|
|
||||||
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
|
# self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
|
||||||
@@ -317,7 +358,7 @@ class ConnectionHandler:
|
|||||||
llm_responses = self.llm.response_with_functions(
|
llm_responses = self.llm.response_with_functions(
|
||||||
self.session_id,
|
self.session_id,
|
||||||
self.dialogue.get_llm_dialogue_with_memory(memory_str),
|
self.dialogue.get_llm_dialogue_with_memory(memory_str),
|
||||||
functions=functions
|
functions=functions,
|
||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
|
self.logger.bind(tag=TAG).error(f"LLM 处理出错 {query}: {e}")
|
||||||
@@ -338,7 +379,9 @@ class ConnectionHandler:
|
|||||||
for response in llm_responses:
|
for response in llm_responses:
|
||||||
content, tools_call = response
|
content, tools_call = response
|
||||||
if content is not None and len(content) > 0:
|
if content is not None and len(content) > 0:
|
||||||
if len(response_message) <= 0 and (content == "```" or "<tool_call>" in content):
|
if len(response_message) <= 0 and (
|
||||||
|
content == "```" or "<tool_call>" in content
|
||||||
|
):
|
||||||
tool_call_flag = True
|
tool_call_flag = True
|
||||||
|
|
||||||
if tools_call is not None:
|
if tools_call is not None:
|
||||||
@@ -360,17 +403,33 @@ class ConnectionHandler:
|
|||||||
break
|
break
|
||||||
|
|
||||||
end_time = time.time()
|
end_time = time.time()
|
||||||
self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
|
self.logger.bind(tag=TAG).debug(
|
||||||
|
f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}"
|
||||||
|
)
|
||||||
if text_index == 0:
|
if text_index == 0:
|
||||||
self.tts.tts_text_queue.put(
|
self.tts.tts_text_queue.put(
|
||||||
TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content=''))
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str,
|
||||||
|
msg_type=MsgType.START_TTS_REQUEST,
|
||||||
|
content="",
|
||||||
|
)
|
||||||
|
)
|
||||||
self.start_tts_request_flag = True
|
self.start_tts_request_flag = True
|
||||||
self.tts.tts_text_queue.put(
|
self.tts.tts_text_queue.put(
|
||||||
TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=content))
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str,
|
||||||
|
msg_type=MsgType.TTS_TEXT_REQUEST,
|
||||||
|
content=content,
|
||||||
|
)
|
||||||
|
)
|
||||||
text_index += 1
|
text_index += 1
|
||||||
if self.start_tts_request_flag:
|
if self.start_tts_request_flag:
|
||||||
self.start_tts_request_flag = False
|
self.start_tts_request_flag = False
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=''))
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
# 处理function call
|
# 处理function call
|
||||||
if tool_call_flag:
|
if tool_call_flag:
|
||||||
@@ -381,7 +440,9 @@ class ConnectionHandler:
|
|||||||
try:
|
try:
|
||||||
content_arguments_json = json.loads(a)
|
content_arguments_json = json.loads(a)
|
||||||
function_name = content_arguments_json["name"]
|
function_name = content_arguments_json["name"]
|
||||||
function_arguments = json.dumps(content_arguments_json["arguments"], ensure_ascii=False)
|
function_arguments = json.dumps(
|
||||||
|
content_arguments_json["arguments"], ensure_ascii=False
|
||||||
|
)
|
||||||
function_id = str(uuid.uuid4().hex)
|
function_id = str(uuid.uuid4().hex)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
bHasError = True
|
bHasError = True
|
||||||
@@ -390,26 +451,35 @@ class ConnectionHandler:
|
|||||||
bHasError = True
|
bHasError = True
|
||||||
response_message.append(content_arguments)
|
response_message.append(content_arguments)
|
||||||
if bHasError:
|
if bHasError:
|
||||||
self.logger.bind(tag=TAG).error(f"function call error: {content_arguments}")
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"function call error: {content_arguments}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
function_arguments = json.loads(function_arguments)
|
function_arguments = json.loads(function_arguments)
|
||||||
if not bHasError:
|
if not bHasError:
|
||||||
self.logger.bind(tag=TAG).info(
|
self.logger.bind(tag=TAG).info(
|
||||||
f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
|
f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}"
|
||||||
|
)
|
||||||
function_call_data = {
|
function_call_data = {
|
||||||
"name": function_name,
|
"name": function_name,
|
||||||
"id": function_id,
|
"id": function_id,
|
||||||
"arguments": function_arguments
|
"arguments": function_arguments,
|
||||||
}
|
}
|
||||||
result = self.func_handler.handle_llm_function_call(self, function_call_data)
|
result = self.func_handler.handle_llm_function_call(
|
||||||
|
self, function_call_data
|
||||||
|
)
|
||||||
self._handle_function_result(result, function_call_data, text_index + 1)
|
self._handle_function_result(result, function_call_data, text_index + 1)
|
||||||
|
|
||||||
# 存储对话内容
|
# 存储对话内容
|
||||||
if len(response_message) > 0:
|
if len(response_message) > 0:
|
||||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
self.dialogue.put(
|
||||||
|
Message(role="assistant", content="".join(response_message))
|
||||||
|
)
|
||||||
|
|
||||||
self.llm_finish_task = True
|
self.llm_finish_task = True
|
||||||
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
|
self.logger.bind(tag=TAG).debug(
|
||||||
|
json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False)
|
||||||
|
)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -417,7 +487,9 @@ class ConnectionHandler:
|
|||||||
if result.action == Action.RESPONSE: # 直接回复前端
|
if result.action == Action.RESPONSE: # 直接回复前端
|
||||||
text = result.response
|
text = result.response
|
||||||
self.recode_first_last_text(text, text_index)
|
self.recode_first_last_text(text, text_index)
|
||||||
asyncio.run_coroutine_threadsafe(self.tts.tts_one_sentence(text), loop=self.loop)
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.tts.tts_one_sentence(text), loop=self.loop
|
||||||
|
)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||||
text = result.result
|
text = result.result
|
||||||
@@ -425,24 +497,40 @@ class ConnectionHandler:
|
|||||||
function_id = function_call_data["id"]
|
function_id = function_call_data["id"]
|
||||||
function_name = function_call_data["name"]
|
function_name = function_call_data["name"]
|
||||||
function_arguments = function_call_data["arguments"]
|
function_arguments = function_call_data["arguments"]
|
||||||
self.dialogue.put(Message(role='assistant',
|
self.dialogue.put(
|
||||||
tool_calls=[{"id": function_id,
|
Message(
|
||||||
"function": {"arguments": function_arguments,
|
role="assistant",
|
||||||
"name": function_name},
|
tool_calls=[
|
||||||
"type": 'function',
|
{
|
||||||
"index": 0}]))
|
"id": function_id,
|
||||||
|
"function": {
|
||||||
|
"arguments": function_arguments,
|
||||||
|
"name": function_name,
|
||||||
|
},
|
||||||
|
"type": "function",
|
||||||
|
"index": 0,
|
||||||
|
}
|
||||||
|
],
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
|
self.dialogue.put(
|
||||||
|
Message(role="tool", tool_call_id=function_id, content=text)
|
||||||
|
)
|
||||||
self.chat_with_function_calling(text, tool_call=True)
|
self.chat_with_function_calling(text, tool_call=True)
|
||||||
elif result.action == Action.NOTFOUND:
|
elif result.action == Action.NOTFOUND:
|
||||||
text = result.result
|
text = result.result
|
||||||
self.recode_first_last_text(text, text_index)
|
self.recode_first_last_text(text, text_index)
|
||||||
asyncio.run_coroutine_threadsafe(self.tts.tts_one_sentence(text), loop=self.loop)
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.tts.tts_one_sentence(text), loop=self.loop
|
||||||
|
)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
else:
|
else:
|
||||||
text = result.result
|
text = result.result
|
||||||
self.recode_first_last_text(text, text_index)
|
self.recode_first_last_text(text, text_index)
|
||||||
asyncio.run_coroutine_threadsafe(self.tts.tts_one_sentence(text), loop=self.loop)
|
asyncio.run_coroutine_threadsafe(
|
||||||
|
self.tts.tts_one_sentence(text), loop=self.loop
|
||||||
|
)
|
||||||
self.dialogue.put(Message(role="assistant", content=text))
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
|
|
||||||
def _tts_priority_thread(self):
|
def _tts_priority_thread(self):
|
||||||
@@ -465,15 +553,23 @@ class ConnectionHandler:
|
|||||||
tts_timeout = self.config.get("tts_timeout", 10)
|
tts_timeout = self.config.get("tts_timeout", 10)
|
||||||
tts_file, text, text_index = future.result(timeout=tts_timeout)
|
tts_file, text, text_index = future.result(timeout=tts_timeout)
|
||||||
if text is None or len(text) <= 0:
|
if text is None or len(text) <= 0:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS出错:{text_index}: tts text is empty")
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"TTS出错:{text_index}: tts text is empty"
|
||||||
|
)
|
||||||
elif tts_file is None:
|
elif tts_file is None:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS出错: file is empty: {text_index}: {text}")
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"TTS出错: file is empty: {text_index}: {text}"
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).debug(f"TTS生成:文件路径: {tts_file}")
|
self.logger.bind(tag=TAG).debug(
|
||||||
|
f"TTS生成:文件路径: {tts_file}"
|
||||||
|
)
|
||||||
if os.path.exists(tts_file):
|
if os.path.exists(tts_file):
|
||||||
opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
|
opus_datas, duration = self.tts.audio_to_opus_data(tts_file)
|
||||||
else:
|
else:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS出错:文件不存在{tts_file}")
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"TTS出错:文件不存在{tts_file}"
|
||||||
|
)
|
||||||
except TimeoutError:
|
except TimeoutError:
|
||||||
self.logger.bind(tag=TAG).error("TTS超时")
|
self.logger.bind(tag=TAG).error("TTS超时")
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -481,16 +577,30 @@ class ConnectionHandler:
|
|||||||
if not self.client_abort:
|
if not self.client_abort:
|
||||||
# 如果没有中途打断就发送语音
|
# 如果没有中途打断就发送语音
|
||||||
self.audio_play_queue.put((opus_datas, text, text_index))
|
self.audio_play_queue.put((opus_datas, text, text_index))
|
||||||
if self.tts.delete_audio_file and tts_file is not None and os.path.exists(tts_file):
|
if (
|
||||||
|
self.tts.delete_audio_file
|
||||||
|
and tts_file is not None
|
||||||
|
and os.path.exists(tts_file)
|
||||||
|
):
|
||||||
os.remove(tts_file)
|
os.remove(tts_file)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"TTS任务处理错误: {e}")
|
self.logger.bind(tag=TAG).error(f"TTS任务处理错误: {e}")
|
||||||
self.clearSpeakStatus()
|
self.clearSpeakStatus()
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": self.session_id})),
|
self.websocket.send(
|
||||||
self.loop
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": "tts",
|
||||||
|
"state": "stop",
|
||||||
|
"session_id": self.session_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
self.loop,
|
||||||
|
)
|
||||||
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"tts_priority priority_thread: {text} {e}"
|
||||||
)
|
)
|
||||||
self.logger.bind(tag=TAG).error(f"tts_priority priority_thread: {text} {e}")
|
|
||||||
|
|
||||||
def _tts_priority_thread_stream(self):
|
def _tts_priority_thread_stream(self):
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
@@ -513,8 +623,16 @@ class ConnectionHandler:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.clearSpeakStatus()
|
self.clearSpeakStatus()
|
||||||
asyncio.run_coroutine_threadsafe(
|
asyncio.run_coroutine_threadsafe(
|
||||||
self.websocket.send(json.dumps({"type": "tts", "state": "stop", "session_id": self.session_id})),
|
self.websocket.send(
|
||||||
self.loop
|
json.dumps(
|
||||||
|
{
|
||||||
|
"type": "tts",
|
||||||
|
"state": "stop",
|
||||||
|
"session_id": self.session_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
),
|
||||||
|
self.loop,
|
||||||
)
|
)
|
||||||
self.logger.error(f"tts_priority priority_thread: {text}{e}")
|
self.logger.error(f"tts_priority priority_thread: {text}{e}")
|
||||||
|
|
||||||
@@ -523,11 +641,14 @@ class ConnectionHandler:
|
|||||||
text = None
|
text = None
|
||||||
try:
|
try:
|
||||||
ttsMessageDTO = self.tts.tts_audio_queue.get()
|
ttsMessageDTO = self.tts.tts_audio_queue.get()
|
||||||
future = asyncio.run_coroutine_threadsafe(sendAudioMessage(self, ttsMessageDTO),
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self.loop)
|
sendAudioMessage(self, ttsMessageDTO), self.loop
|
||||||
|
)
|
||||||
future.result()
|
future.result()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"audio_play_priority priority_thread: {text} {e}")
|
self.logger.bind(tag=TAG).error(
|
||||||
|
f"audio_play_priority priority_thread: {text} {e}"
|
||||||
|
)
|
||||||
|
|
||||||
def speak_and_play(self, text, text_index=0):
|
def speak_and_play(self, text, text_index=0):
|
||||||
if text is None or len(text) <= 0:
|
if text is None or len(text) <= 0:
|
||||||
|
|||||||
@@ -6,7 +6,10 @@ import asyncio
|
|||||||
import time
|
import time
|
||||||
|
|
||||||
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, MsgType
|
from core.providers.tts.dto.dto import TTSMessageDTO, SentenceType, MsgType
|
||||||
from core.utils.util import remove_punctuation_and_length, get_string_no_punctuation_or_emoji
|
from core.utils.util import (
|
||||||
|
remove_punctuation_and_length,
|
||||||
|
get_string_no_punctuation_or_emoji,
|
||||||
|
)
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -14,7 +17,9 @@ logger = setup_logging()
|
|||||||
|
|
||||||
async def sendAudioMessage(conn, ttsMessageDTO: TTSMessageDTO):
|
async def sendAudioMessage(conn, ttsMessageDTO: TTSMessageDTO):
|
||||||
if ttsMessageDTO.u_id != conn.u_id:
|
if ttsMessageDTO.u_id != conn.u_id:
|
||||||
logger.bind(tag=TAG).info(f"msg id:{ttsMessageDTO.u_id},不是当前对话,当前对话id:{conn.u_id}")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"msg id:{ttsMessageDTO.u_id},不是当前对话,当前对话id:{conn.u_id}"
|
||||||
|
)
|
||||||
return
|
return
|
||||||
# 发送句子开始消息
|
# 发送句子开始消息
|
||||||
if SentenceType.SENTENCE_START == ttsMessageDTO.sentence_type:
|
if SentenceType.SENTENCE_START == ttsMessageDTO.sentence_type:
|
||||||
@@ -25,7 +30,9 @@ async def sendAudioMessage(conn, ttsMessageDTO: TTSMessageDTO):
|
|||||||
original_frame_duration = 60 # 原始帧时长(毫秒)
|
original_frame_duration = 60 # 原始帧时长(毫秒)
|
||||||
adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
|
adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
|
||||||
total_frames = len(ttsMessageDTO.content) # 获取总帧数
|
total_frames = len(ttsMessageDTO.content) # 获取总帧数
|
||||||
compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
|
compensation = (
|
||||||
|
total_frames * (original_frame_duration - adjusted_frame_duration) / 1000
|
||||||
|
) # 补偿时间(秒)
|
||||||
|
|
||||||
start_time = time.perf_counter()
|
start_time = time.perf_counter()
|
||||||
play_position = 0 # 已播放时长(毫秒)
|
play_position = 0 # 已播放时长(毫秒)
|
||||||
@@ -55,18 +62,14 @@ async def sendAudioMessage(conn, ttsMessageDTO: TTSMessageDTO):
|
|||||||
|
|
||||||
# 发送结束消息(如果是最后一个文本)
|
# 发送结束消息(如果是最后一个文本)
|
||||||
if conn.llm_finish_task and MsgType.STOP_TTS_RESPONSE == ttsMessageDTO.msg_type:
|
if conn.llm_finish_task and MsgType.STOP_TTS_RESPONSE == ttsMessageDTO.msg_type:
|
||||||
await send_tts_message(conn, 'stop', None)
|
await send_tts_message(conn, "stop", None)
|
||||||
if conn.close_after_chat:
|
if conn.close_after_chat:
|
||||||
await conn.close()
|
await conn.close()
|
||||||
|
|
||||||
|
|
||||||
async def send_tts_message(conn, state, text=None):
|
async def send_tts_message(conn, state, text=None):
|
||||||
"""发送 TTS 状态消息"""
|
"""发送 TTS 状态消息"""
|
||||||
message = {
|
message = {"type": "tts", "state": state, "session_id": conn.session_id}
|
||||||
"type": "tts",
|
|
||||||
"state": state,
|
|
||||||
"session_id": conn.session_id
|
|
||||||
}
|
|
||||||
if text is not None:
|
if text is not None:
|
||||||
message["text"] = text
|
message["text"] = text
|
||||||
|
|
||||||
@@ -78,16 +81,17 @@ async def send_tts_message(conn, state, text=None):
|
|||||||
async def send_stt_message(conn, text):
|
async def send_stt_message(conn, text):
|
||||||
"""发送 STT 状态消息"""
|
"""发送 STT 状态消息"""
|
||||||
stt_text = get_string_no_punctuation_or_emoji(text)
|
stt_text = get_string_no_punctuation_or_emoji(text)
|
||||||
await conn.websocket.send(json.dumps({
|
|
||||||
"type": "stt",
|
|
||||||
"text": stt_text,
|
|
||||||
"session_id": conn.session_id}
|
|
||||||
))
|
|
||||||
await conn.websocket.send(
|
await conn.websocket.send(
|
||||||
json.dumps({
|
json.dumps({"type": "stt", "text": stt_text, "session_id": conn.session_id})
|
||||||
"type": "llm",
|
)
|
||||||
"text": "😊",
|
await conn.websocket.send(
|
||||||
"emotion": "happy",
|
json.dumps(
|
||||||
"session_id": conn.session_id}
|
{
|
||||||
))
|
"type": "llm",
|
||||||
|
"text": "😊",
|
||||||
|
"emotion": "happy",
|
||||||
|
"session_id": conn.session_id,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
await send_tts_message(conn, "start")
|
await send_tts_message(conn, "start")
|
||||||
|
|||||||
@@ -24,52 +24,65 @@ class AccessToken:
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_text(text):
|
def _encode_text(text):
|
||||||
encoded_text = parse.quote_plus(text)
|
encoded_text = parse.quote_plus(text)
|
||||||
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
|
return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def _encode_dict(dic):
|
def _encode_dict(dic):
|
||||||
keys = dic.keys()
|
keys = dic.keys()
|
||||||
dic_sorted = [(key, dic[key]) for key in sorted(keys)]
|
dic_sorted = [(key, dic[key]) for key in sorted(keys)]
|
||||||
encoded_text = parse.urlencode(dic_sorted)
|
encoded_text = parse.urlencode(dic_sorted)
|
||||||
return encoded_text.replace('+', '%20').replace('*', '%2A').replace('%7E', '~')
|
return encoded_text.replace("+", "%20").replace("*", "%2A").replace("%7E", "~")
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def create_token(access_key_id, access_key_secret):
|
def create_token(access_key_id, access_key_secret):
|
||||||
parameters = {'AccessKeyId': access_key_id,
|
parameters = {
|
||||||
'Action': 'CreateToken',
|
"AccessKeyId": access_key_id,
|
||||||
'Format': 'JSON',
|
"Action": "CreateToken",
|
||||||
'RegionId': 'cn-shanghai',
|
"Format": "JSON",
|
||||||
'SignatureMethod': 'HMAC-SHA1',
|
"RegionId": "cn-shanghai",
|
||||||
'SignatureNonce': str(uuid.uuid1()),
|
"SignatureMethod": "HMAC-SHA1",
|
||||||
'SignatureVersion': '1.0',
|
"SignatureNonce": str(uuid.uuid1()),
|
||||||
'Timestamp': time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
"SignatureVersion": "1.0",
|
||||||
'Version': '2019-02-28'}
|
"Timestamp": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()),
|
||||||
|
"Version": "2019-02-28",
|
||||||
|
}
|
||||||
# 构造规范化的请求字符串
|
# 构造规范化的请求字符串
|
||||||
query_string = AccessToken._encode_dict(parameters)
|
query_string = AccessToken._encode_dict(parameters)
|
||||||
print('规范化的请求字符串: %s' % query_string)
|
print("规范化的请求字符串: %s" % query_string)
|
||||||
# 构造待签名字符串
|
# 构造待签名字符串
|
||||||
string_to_sign = 'GET' + '&' + AccessToken._encode_text('/') + '&' + AccessToken._encode_text(query_string)
|
string_to_sign = (
|
||||||
print('待签名的字符串: %s' % string_to_sign)
|
"GET"
|
||||||
|
+ "&"
|
||||||
|
+ AccessToken._encode_text("/")
|
||||||
|
+ "&"
|
||||||
|
+ AccessToken._encode_text(query_string)
|
||||||
|
)
|
||||||
|
print("待签名的字符串: %s" % string_to_sign)
|
||||||
# 计算签名
|
# 计算签名
|
||||||
secreted_string = hmac.new(bytes(access_key_secret + '&', encoding='utf-8'),
|
secreted_string = hmac.new(
|
||||||
bytes(string_to_sign, encoding='utf-8'),
|
bytes(access_key_secret + "&", encoding="utf-8"),
|
||||||
hashlib.sha1).digest()
|
bytes(string_to_sign, encoding="utf-8"),
|
||||||
|
hashlib.sha1,
|
||||||
|
).digest()
|
||||||
signature = base64.b64encode(secreted_string)
|
signature = base64.b64encode(secreted_string)
|
||||||
print('签名: %s' % signature)
|
print("签名: %s" % signature)
|
||||||
# 进行URL编码
|
# 进行URL编码
|
||||||
signature = AccessToken._encode_text(signature)
|
signature = AccessToken._encode_text(signature)
|
||||||
print('URL编码后的签名: %s' % signature)
|
print("URL编码后的签名: %s" % signature)
|
||||||
# 调用服务
|
# 调用服务
|
||||||
full_url = 'http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s' % (signature, query_string)
|
full_url = "http://nls-meta.cn-shanghai.aliyuncs.com/?Signature=%s&%s" % (
|
||||||
print('url: %s' % full_url)
|
signature,
|
||||||
|
query_string,
|
||||||
|
)
|
||||||
|
print("url: %s" % full_url)
|
||||||
# 提交HTTP GET请求
|
# 提交HTTP GET请求
|
||||||
response = requests.get(full_url)
|
response = requests.get(full_url)
|
||||||
if response.ok:
|
if response.ok:
|
||||||
root_obj = response.json()
|
root_obj = response.json()
|
||||||
key = 'Token'
|
key = "Token"
|
||||||
if key in root_obj:
|
if key in root_obj:
|
||||||
token = root_obj[key]['Id']
|
token = root_obj[key]["Id"]
|
||||||
expire_time = root_obj[key]['ExpireTime']
|
expire_time = root_obj[key]["ExpireTime"]
|
||||||
return token, expire_time
|
return token, expire_time
|
||||||
print(response.text)
|
print(response.text)
|
||||||
return None, None
|
return None, None
|
||||||
@@ -77,22 +90,23 @@ class AccessToken:
|
|||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
|
|
||||||
|
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
|
|
||||||
# 新增空值判断逻辑
|
# 新增空值判断逻辑
|
||||||
access_key_id = config.get("access_key_id")
|
access_key_id = config.get("access_key_id")
|
||||||
access_key_secret = config.get("access_key_secret")
|
access_key_secret = config.get("access_key_secret")
|
||||||
if access_key_id and access_key_secret:
|
if access_key_id and access_key_secret:
|
||||||
# 使用密钥对生成临时token
|
# 使用密钥对生成临时token
|
||||||
token, expire_time = AccessToken.create_token(access_key_id, access_key_secret)
|
token, expire_time = AccessToken.create_token(
|
||||||
|
access_key_id, access_key_secret
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
# 直接使用预生成的长期token
|
# 直接使用预生成的长期token
|
||||||
token = config.get("token")
|
token = config.get("token")
|
||||||
expire_time = None
|
expire_time = None
|
||||||
|
|
||||||
print('token: %s, expire time(s): %s' % (token, expire_time))
|
print("token: %s, expire time(s): %s" % (token, expire_time))
|
||||||
|
|
||||||
self.appkey = config.get("appkey")
|
self.appkey = config.get("appkey")
|
||||||
self.token = token
|
self.token = token
|
||||||
@@ -105,12 +119,13 @@ class TTSProvider(TTSProviderBase):
|
|||||||
|
|
||||||
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
|
self.host = config.get("host", "nls-gateway-cn-shanghai.aliyuncs.com")
|
||||||
self.api_url = f"https://{self.host}/stream/v1/tts"
|
self.api_url = f"https://{self.host}/stream/v1/tts"
|
||||||
self.header = {
|
self.header = {"Content-Type": "application/json"}
|
||||||
"Content-Type": "application/json"
|
|
||||||
}
|
|
||||||
|
|
||||||
def generate_filename(self, extension=".wav"):
|
def generate_filename(self, extension=".wav"):
|
||||||
return os.path.join(self.output_file, f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}")
|
return os.path.join(
|
||||||
|
self.output_file,
|
||||||
|
f"tts-{__name__}{datetime.now().date()}@{uuid.uuid4().hex}{extension}",
|
||||||
|
)
|
||||||
|
|
||||||
async def text_to_speak(self, u_id, text, is_last_text=False, is_first_text=False):
|
async def text_to_speak(self, u_id, text, is_last_text=False, is_first_text=False):
|
||||||
request_json = {
|
request_json = {
|
||||||
@@ -122,25 +137,34 @@ class TTSProvider(TTSProviderBase):
|
|||||||
"voice": self.voice,
|
"voice": self.voice,
|
||||||
"volume": self.volume,
|
"volume": self.volume,
|
||||||
"speech_rate": self.speech_rate,
|
"speech_rate": self.speech_rate,
|
||||||
"pitch_rate": self.pitch_rate
|
"pitch_rate": self.pitch_rate,
|
||||||
}
|
}
|
||||||
|
|
||||||
print(self.api_url, json.dumps(request_json, ensure_ascii=False))
|
print(self.api_url, json.dumps(request_json, ensure_ascii=False))
|
||||||
tmp_file = self.generate_filename()
|
tmp_file = self.generate_filename()
|
||||||
try:
|
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
|
||||||
|
)
|
||||||
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
|
# 检查返回请求数据的mime类型是否是audio/***,是则保存到指定路径下;返回的是binary格式的
|
||||||
if resp.headers['Content-Type'].startswith('audio/'):
|
if resp.headers["Content-Type"].startswith("audio/"):
|
||||||
with open(tmp_file, 'wb') as f:
|
with open(tmp_file, "wb") as f:
|
||||||
f.write(resp.content)
|
f.write(resp.content)
|
||||||
else:
|
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}"
|
||||||
|
)
|
||||||
# 使用 pydub 读取临时文件
|
# 使用 pydub 读取临时文件
|
||||||
audio = AudioSegment.from_file(tmp_file, format="wav")
|
audio = AudioSegment.from_file(tmp_file, format="wav")
|
||||||
audio = audio.set_channels(1).set_frame_rate(16000)
|
audio = audio.set_channels(1).set_frame_rate(16000)
|
||||||
opus_datas = self.wav_to_opus_data_audio_raw(audio.raw_data)
|
opus_datas = self.wav_to_opus_data_audio_raw(audio.raw_data)
|
||||||
yield TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_datas,
|
yield TTSMessageDTO(
|
||||||
tts_finish_text=text, sentence_type=SentenceType.SENTENCE_START)
|
u_id=u_id,
|
||||||
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=opus_datas,
|
||||||
|
tts_finish_text=text,
|
||||||
|
sentence_type=SentenceType.SENTENCE_START,
|
||||||
|
)
|
||||||
# 用完后删除临时文件
|
# 用完后删除临时文件
|
||||||
try:
|
try:
|
||||||
os.remove(tmp_file)
|
os.remove(tmp_file)
|
||||||
|
|||||||
@@ -35,11 +35,25 @@ class TTSProviderBase(ABC):
|
|||||||
self.stop_event = threading.Event()
|
self.stop_event = threading.Event()
|
||||||
|
|
||||||
self.tts_text_buff = []
|
self.tts_text_buff = []
|
||||||
self.punctuations = ("。", "?", "!", ";", ":", ".", "?", "!", ";", ":", " ", ",", ",")
|
self.punctuations = (
|
||||||
|
"。",
|
||||||
|
"?",
|
||||||
|
"!",
|
||||||
|
";",
|
||||||
|
":",
|
||||||
|
".",
|
||||||
|
"?",
|
||||||
|
"!",
|
||||||
|
";",
|
||||||
|
":",
|
||||||
|
" ",
|
||||||
|
",",
|
||||||
|
",",
|
||||||
|
)
|
||||||
self.tts_request = False
|
self.tts_request = False
|
||||||
self.processed_chars = 0
|
self.processed_chars = 0
|
||||||
self.stream = False
|
self.stream = False
|
||||||
self.last_to_opus_raw = b''
|
self.last_to_opus_raw = b""
|
||||||
|
|
||||||
# 启动tts_text_queue监听线程
|
# 启动tts_text_queue监听线程
|
||||||
# 线程任务相关
|
# 线程任务相关
|
||||||
@@ -51,7 +65,9 @@ class TTSProviderBase(ABC):
|
|||||||
|
|
||||||
async def open_audio_channels(self):
|
async def open_audio_channels(self):
|
||||||
# 启动tts_text_queue监听线程
|
# 启动tts_text_queue监听线程
|
||||||
tts_priority = threading.Thread(target=self._tts_text_priority_thread, daemon=True)
|
tts_priority = threading.Thread(
|
||||||
|
target=self._tts_text_priority_thread, daemon=True
|
||||||
|
)
|
||||||
tts_priority.start()
|
tts_priority.start()
|
||||||
|
|
||||||
async def stop_listen_resource(self):
|
async def stop_listen_resource(self):
|
||||||
@@ -67,15 +83,19 @@ class TTSProviderBase(ABC):
|
|||||||
def _get_segment_text(self):
|
def _get_segment_text(self):
|
||||||
# 合并当前全部文本并处理未分割部分
|
# 合并当前全部文本并处理未分割部分
|
||||||
full_text = "".join(self.tts_text_buff)
|
full_text = "".join(self.tts_text_buff)
|
||||||
current_text = full_text[self.processed_chars:] # 从未处理的位置开始
|
current_text = full_text[self.processed_chars :] # 从未处理的位置开始
|
||||||
last_punct_pos = -1
|
last_punct_pos = -1
|
||||||
for punct in self.punctuations:
|
for punct in self.punctuations:
|
||||||
pos = current_text.rfind(punct)
|
pos = current_text.rfind(punct)
|
||||||
if (pos != -1 and last_punct_pos == -1) or (pos != -1 and pos < last_punct_pos):
|
if (pos != -1 and last_punct_pos == -1) or (
|
||||||
|
pos != -1 and pos < last_punct_pos
|
||||||
|
):
|
||||||
last_punct_pos = pos
|
last_punct_pos = pos
|
||||||
if last_punct_pos != -1:
|
if last_punct_pos != -1:
|
||||||
segment_text_raw = current_text[:last_punct_pos + 1]
|
segment_text_raw = current_text[: last_punct_pos + 1]
|
||||||
segment_text = textUtils.get_string_no_punctuation_or_emoji(segment_text_raw)
|
segment_text = textUtils.get_string_no_punctuation_or_emoji(
|
||||||
|
segment_text_raw
|
||||||
|
)
|
||||||
self.processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
self.processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
||||||
return segment_text
|
return segment_text
|
||||||
else:
|
else:
|
||||||
@@ -98,11 +118,21 @@ class TTSProviderBase(ABC):
|
|||||||
async def finish_session(self, session_id):
|
async def finish_session(self, session_id):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
async def tts_one_sentence(self,text):
|
async def tts_one_sentence(self, text):
|
||||||
uuid_str = str(uuid.uuid4()).replace("-", "")
|
uuid_str = str(uuid.uuid4()).replace("-", "")
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content=''))
|
self.tts.tts_text_queue.put(
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=text))
|
TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.START_TTS_REQUEST, content="")
|
||||||
self.tts.tts_text_queue.put(TTSMessageDTO(u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=text))
|
)
|
||||||
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str, msg_type=MsgType.TTS_TEXT_REQUEST, content=text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.tts.tts_text_queue.put(
|
||||||
|
TTSMessageDTO(
|
||||||
|
u_id=uuid_str, msg_type=MsgType.STOP_TTS_REQUEST, content=text
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def _enable_two_way_tts(self):
|
def _enable_two_way_tts(self):
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
@@ -114,27 +144,36 @@ class TTSProviderBase(ABC):
|
|||||||
self.tts_request = True
|
self.tts_request = True
|
||||||
self.u_id = ttsMessageDTO.u_id
|
self.u_id = ttsMessageDTO.u_id
|
||||||
# 开启session
|
# 开启session
|
||||||
future = asyncio.run_coroutine_threadsafe(self.start_session(ttsMessageDTO.u_id), loop=self.loop)
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self.start_session(ttsMessageDTO.u_id), loop=self.loop
|
||||||
|
)
|
||||||
future.result()
|
future.result()
|
||||||
# await self.start_session(ttsMessageDTO.u_id)
|
# await self.start_session(ttsMessageDTO.u_id)
|
||||||
elif self.tts_request and msg_type == MsgType.TTS_TEXT_REQUEST:
|
elif self.tts_request and msg_type == MsgType.TTS_TEXT_REQUEST:
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
self.text_to_speak(u_id=ttsMessageDTO.u_id, text=ttsMessageDTO.content), loop=self.loop)
|
self.text_to_speak(
|
||||||
|
u_id=ttsMessageDTO.u_id, text=ttsMessageDTO.content
|
||||||
|
),
|
||||||
|
loop=self.loop,
|
||||||
|
)
|
||||||
future.result()
|
future.result()
|
||||||
elif msg_type == MsgType.STOP_TTS_REQUEST:
|
elif msg_type == MsgType.STOP_TTS_REQUEST:
|
||||||
self.tts_request = False
|
self.tts_request = False
|
||||||
future = asyncio.run_coroutine_threadsafe(self.finish_session(ttsMessageDTO.u_id), loop=self.loop)
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self.finish_session(ttsMessageDTO.u_id), loop=self.loop
|
||||||
|
)
|
||||||
future.result()
|
future.result()
|
||||||
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Failed to process TTS text: {e}")
|
logger.bind(tag=TAG).error(f"Failed to process TTS text: {e}")
|
||||||
# 报错了。要关闭说话
|
# 报错了。要关闭说话
|
||||||
self.tts_audio_queue.put(
|
self.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=self.u_id, msg_type=MsgType.STOP_TTS_RESPONSE, content=[],
|
u_id=self.u_id,
|
||||||
tts_finish_text='',
|
msg_type=MsgType.STOP_TTS_RESPONSE,
|
||||||
sentence_type=None
|
content=[],
|
||||||
|
tts_finish_text="",
|
||||||
|
sentence_type=None,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
@@ -162,25 +201,43 @@ class TTSProviderBase(ABC):
|
|||||||
if segment_text:
|
if segment_text:
|
||||||
# 修改部分:创建协程对象
|
# 修改部分:创建协程对象
|
||||||
# 修改部分:创建协程对象
|
# 修改部分:创建协程对象
|
||||||
tts_generator = self.text_to_speak(ttsMessageDTO.u_id, segment_text,
|
tts_generator = self.text_to_speak(
|
||||||
True if msg_type == MsgType.STOP_TTS_REQUEST else False,
|
ttsMessageDTO.u_id,
|
||||||
True if msg_type == MsgType.START_TTS_REQUEST else False)
|
segment_text,
|
||||||
future = asyncio.run_coroutine_threadsafe(self.process_generator(tts_generator), self.loop)
|
True if msg_type == MsgType.STOP_TTS_REQUEST else False,
|
||||||
|
(
|
||||||
|
True
|
||||||
|
if msg_type == MsgType.START_TTS_REQUEST
|
||||||
|
else False
|
||||||
|
),
|
||||||
|
)
|
||||||
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
|
self.process_generator(tts_generator), self.loop
|
||||||
|
)
|
||||||
self.active_tasks.add(future)
|
self.active_tasks.add(future)
|
||||||
if self.active_tasks:
|
if self.active_tasks:
|
||||||
|
|
||||||
async def wrap_future(future):
|
async def wrap_future(future):
|
||||||
return await asyncio.wrap_future(future)
|
return await asyncio.wrap_future(future)
|
||||||
|
|
||||||
wrapped_tasks = [wrap_future(task) for task in self.active_tasks]
|
wrapped_tasks = [
|
||||||
done, _ = loop.run_until_complete(asyncio.wait(wrapped_tasks))
|
wrap_future(task) for task in self.active_tasks
|
||||||
|
]
|
||||||
|
done, _ = loop.run_until_complete(
|
||||||
|
asyncio.wait(wrapped_tasks)
|
||||||
|
)
|
||||||
self.active_tasks -= done
|
self.active_tasks -= done
|
||||||
|
|
||||||
# 发送合成结束
|
# 发送合成结束
|
||||||
self.tts_audio_queue.put(TTSMessageDTO(u_id=ttsMessageDTO.u_id,
|
self.tts_audio_queue.put(
|
||||||
msg_type=MsgType.STOP_TTS_RESPONSE,
|
TTSMessageDTO(
|
||||||
content=[],
|
u_id=ttsMessageDTO.u_id,
|
||||||
tts_finish_text='',
|
msg_type=MsgType.STOP_TTS_RESPONSE,
|
||||||
sentence_type=SentenceType.SENTENCE_END))
|
content=[],
|
||||||
|
tts_finish_text="",
|
||||||
|
sentence_type=SentenceType.SENTENCE_END,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
segment_text = self._get_segment_text()
|
segment_text = self._get_segment_text()
|
||||||
if segment_text:
|
if segment_text:
|
||||||
@@ -189,25 +246,31 @@ class TTSProviderBase(ABC):
|
|||||||
ttsMessageDTO.u_id,
|
ttsMessageDTO.u_id,
|
||||||
segment_text,
|
segment_text,
|
||||||
msg_type == MsgType.STOP_TTS_REQUEST,
|
msg_type == MsgType.STOP_TTS_REQUEST,
|
||||||
msg_type == MsgType.START_TTS_REQUEST
|
msg_type == MsgType.START_TTS_REQUEST,
|
||||||
)
|
)
|
||||||
# 提交协程到事件循环
|
# 提交协程到事件循环
|
||||||
tts_generator_future = asyncio.run_coroutine_threadsafe(
|
tts_generator_future = asyncio.run_coroutine_threadsafe(
|
||||||
self.process_generator(tts_generator),
|
self.process_generator(tts_generator), loop
|
||||||
loop
|
|
||||||
)
|
)
|
||||||
self.active_tasks.add(tts_generator_future)
|
self.active_tasks.add(tts_generator_future)
|
||||||
if len(self.active_tasks) >= self.max_workers:
|
if len(self.active_tasks) >= self.max_workers:
|
||||||
# 等待所有任务完成
|
# 等待所有任务完成
|
||||||
try:
|
try:
|
||||||
|
|
||||||
async def wrap_future(future):
|
async def wrap_future(future):
|
||||||
return await asyncio.wrap_future(future)
|
return await asyncio.wrap_future(future)
|
||||||
|
|
||||||
wrapped_tasks = [wrap_future(task) for task in self.active_tasks]
|
wrapped_tasks = [
|
||||||
done, _ = loop.run_until_complete(asyncio.wait(wrapped_tasks))
|
wrap_future(task) for task in self.active_tasks
|
||||||
|
]
|
||||||
|
done, _ = loop.run_until_complete(
|
||||||
|
asyncio.wait(wrapped_tasks)
|
||||||
|
)
|
||||||
self.active_tasks -= done
|
self.active_tasks -= done
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Failed to process TTS text: {e}")
|
logger.bind(tag=TAG).error(
|
||||||
|
f"Failed to process TTS text: {e}"
|
||||||
|
)
|
||||||
traceback.print_exc()
|
traceback.print_exc()
|
||||||
else:
|
else:
|
||||||
pass
|
pass
|
||||||
@@ -227,10 +290,14 @@ class TTSProviderBase(ABC):
|
|||||||
asyncio.run(self.text_to_speak(text, tmp_file))
|
asyncio.run(self.text_to_speak(text, tmp_file))
|
||||||
if not os.path.exists(tmp_file):
|
if not os.path.exists(tmp_file):
|
||||||
max_repeat_time = max_repeat_time - 1
|
max_repeat_time = max_repeat_time - 1
|
||||||
logger.bind(tag=TAG).error(f"语音生成失败: {text}:{tmp_file},再试{max_repeat_time}次")
|
logger.bind(tag=TAG).error(
|
||||||
|
f"语音生成失败: {text}:{tmp_file},再试{max_repeat_time}次"
|
||||||
|
)
|
||||||
|
|
||||||
if max_repeat_time > 0:
|
if max_repeat_time > 0:
|
||||||
logger.bind(tag=TAG).info(f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次")
|
logger.bind(tag=TAG).info(
|
||||||
|
f"语音生成成功: {text}:{tmp_file},重试{5 - max_repeat_time}次"
|
||||||
|
)
|
||||||
|
|
||||||
return tmp_file
|
return tmp_file
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@@ -256,7 +323,7 @@ class TTSProviderBase(ABC):
|
|||||||
# 获取文件后缀名
|
# 获取文件后缀名
|
||||||
file_type = os.path.splitext(audio_file_path)[1]
|
file_type = os.path.splitext(audio_file_path)[1]
|
||||||
if file_type:
|
if file_type:
|
||||||
file_type = file_type.lstrip('.')
|
file_type = file_type.lstrip(".")
|
||||||
audio = AudioSegment.from_file(audio_file_path, format=file_type)
|
audio = AudioSegment.from_file(audio_file_path, format=file_type)
|
||||||
|
|
||||||
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
|
# 转换为单声道/16kHz采样率/16位小端编码(确保与编码器匹配)
|
||||||
@@ -279,11 +346,11 @@ class TTSProviderBase(ABC):
|
|||||||
# 按帧处理所有音频数据(包括最后一帧可能补零)
|
# 按帧处理所有音频数据(包括最后一帧可能补零)
|
||||||
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
|
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
|
||||||
# 获取当前帧的二进制数据
|
# 获取当前帧的二进制数据
|
||||||
chunk = raw_data[i:i + frame_size * 2]
|
chunk = raw_data[i : i + frame_size * 2]
|
||||||
|
|
||||||
# 如果最后一帧不足,补零
|
# 如果最后一帧不足,补零
|
||||||
if len(chunk) < frame_size * 2:
|
if len(chunk) < frame_size * 2:
|
||||||
chunk += b'\x00' * (frame_size * 2 - len(chunk))
|
chunk += b"\x00" * (frame_size * 2 - len(chunk))
|
||||||
|
|
||||||
# 转换为numpy数组处理
|
# 转换为numpy数组处理
|
||||||
np_frame = np.frombuffer(chunk, dtype=np.int16)
|
np_frame = np.frombuffer(chunk, dtype=np.int16)
|
||||||
@@ -295,7 +362,9 @@ class TTSProviderBase(ABC):
|
|||||||
return opus_datas, duration
|
return opus_datas, duration
|
||||||
|
|
||||||
def get_audio_from_tts(self, data_bytes, src_rate, to_rate=16000):
|
def get_audio_from_tts(self, data_bytes, src_rate, to_rate=16000):
|
||||||
tts_speech = torch.from_numpy(np.array(np.frombuffer(data_bytes, dtype=np.int16))).unsqueeze(dim=0)
|
tts_speech = torch.from_numpy(
|
||||||
|
np.array(np.frombuffer(data_bytes, dtype=np.int16))
|
||||||
|
).unsqueeze(dim=0)
|
||||||
with io.BytesIO() as bf:
|
with io.BytesIO() as bf:
|
||||||
torchaudio.save(bf, tts_speech, src_rate, format="wav")
|
torchaudio.save(bf, tts_speech, src_rate, format="wav")
|
||||||
audio = AudioSegment.from_file(bf, format="wav")
|
audio = AudioSegment.from_file(bf, format="wav")
|
||||||
@@ -304,7 +373,7 @@ class TTSProviderBase(ABC):
|
|||||||
|
|
||||||
def wav_to_opus_data_audio_raw(self, raw_data_var, is_end=False):
|
def wav_to_opus_data_audio_raw(self, raw_data_var, is_end=False):
|
||||||
raw_data = self.last_to_opus_raw + raw_data_var
|
raw_data = self.last_to_opus_raw + raw_data_var
|
||||||
self.last_to_opus_raw = b''
|
self.last_to_opus_raw = b""
|
||||||
# 初始化Opus编码器
|
# 初始化Opus编码器
|
||||||
encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO)
|
encoder = opuslib_next.Encoder(16000, 1, opuslib_next.APPLICATION_AUDIO)
|
||||||
|
|
||||||
@@ -316,7 +385,7 @@ class TTSProviderBase(ABC):
|
|||||||
# 按帧处理所有音频数据(包括最后一帧可能补零)
|
# 按帧处理所有音频数据(包括最后一帧可能补零)
|
||||||
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
|
for i in range(0, len(raw_data), frame_size * 2): # 16bit=2bytes/sample
|
||||||
# 获取当前帧的二进制数据
|
# 获取当前帧的二进制数据
|
||||||
chunk = raw_data[i:i + frame_size * 2]
|
chunk = raw_data[i : i + frame_size * 2]
|
||||||
|
|
||||||
# 如果最后一帧不足,补零
|
# 如果最后一帧不足,补零
|
||||||
# 缓存记录一下
|
# 缓存记录一下
|
||||||
@@ -326,7 +395,7 @@ class TTSProviderBase(ABC):
|
|||||||
break
|
break
|
||||||
if len(chunk) < frame_size * 2 and is_end:
|
if len(chunk) < frame_size * 2 and is_end:
|
||||||
logger.bind(tag=TAG).info("是最后一句了,补零")
|
logger.bind(tag=TAG).info("是最后一句了,补零")
|
||||||
chunk += b'\x00' * (frame_size * 2 - len(chunk))
|
chunk += b"\x00" * (frame_size * 2 - len(chunk))
|
||||||
|
|
||||||
# 转换为numpy数组处理
|
# 转换为numpy数组处理
|
||||||
np_frame = np.frombuffer(chunk, dtype=np.int16)
|
np_frame = np.frombuffer(chunk, dtype=np.int16)
|
||||||
|
|||||||
@@ -13,17 +13,23 @@ from core.providers.tts.dto.dto import TTSMessageDTO, MsgType, SentenceType
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class TTSProvider(TTSProviderBase):
|
class TTSProvider(TTSProviderBase):
|
||||||
def __init__(self, config, delete_audio_file):
|
def __init__(self, config, delete_audio_file):
|
||||||
super().__init__(config, delete_audio_file)
|
super().__init__(config, delete_audio_file)
|
||||||
self.voice = config.get("voice")
|
self.voice = config.get("voice")
|
||||||
|
|
||||||
def generate_filename(self, extension=".mp3"):
|
def generate_filename(self, extension=".mp3"):
|
||||||
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, u_id, text, is_last_text=False, is_first_text=False):
|
async def text_to_speak(self, u_id, text, is_last_text=False, is_first_text=False):
|
||||||
try:
|
try:
|
||||||
communicate = edge_tts.Communicate(text, voice=self.voice) # Use your preferred voice
|
communicate = edge_tts.Communicate(
|
||||||
|
text, voice=self.voice
|
||||||
|
) # Use your preferred voice
|
||||||
tmp_file = self.generate_filename()
|
tmp_file = self.generate_filename()
|
||||||
await communicate.save(tmp_file)
|
await communicate.save(tmp_file)
|
||||||
|
|
||||||
@@ -31,7 +37,13 @@ class TTSProvider(TTSProviderBase):
|
|||||||
audio = AudioSegment.from_file(tmp_file, format="mp3")
|
audio = AudioSegment.from_file(tmp_file, format="mp3")
|
||||||
audio = audio.set_channels(1).set_frame_rate(16000)
|
audio = audio.set_channels(1).set_frame_rate(16000)
|
||||||
opus_datas = self.wav_to_opus_data_audio_raw(audio.raw_data)
|
opus_datas = self.wav_to_opus_data_audio_raw(audio.raw_data)
|
||||||
yield TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_datas, tts_finish_text=text,sentence_type=SentenceType.SENTENCE_START)
|
yield TTSMessageDTO(
|
||||||
|
u_id=u_id,
|
||||||
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=opus_datas,
|
||||||
|
tts_finish_text=text,
|
||||||
|
sentence_type=SentenceType.SENTENCE_START,
|
||||||
|
)
|
||||||
# 用完后删除临时文件
|
# 用完后删除临时文件
|
||||||
try:
|
try:
|
||||||
os.remove(tmp_file)
|
os.remove(tmp_file)
|
||||||
@@ -40,6 +52,10 @@ class TTSProvider(TTSProviderBase):
|
|||||||
pass
|
pass
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"TTSProvider text_to_speak error: {e}")
|
logger.bind(tag=TAG).error(f"TTSProvider text_to_speak error: {e}")
|
||||||
yield TTSMessageDTO(u_id=u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=[], tts_finish_text=text,sentence_type=SentenceType.SENTENCE_START)
|
yield TTSMessageDTO(
|
||||||
|
u_id=u_id,
|
||||||
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=[],
|
||||||
|
tts_finish_text=text,
|
||||||
|
sentence_type=SentenceType.SENTENCE_START,
|
||||||
|
)
|
||||||
|
|||||||
@@ -12,50 +12,72 @@ class WebSocketServer:
|
|||||||
def __init__(self, config: dict):
|
def __init__(self, config: dict):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.logger = setup_logging()
|
self.logger = setup_logging()
|
||||||
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = self._create_processing_instances()
|
self._vad, self._asr, self._llm, self._tts, self._memory, self.intent = (
|
||||||
|
self._create_processing_instances()
|
||||||
|
)
|
||||||
self.active_connections = set() # 添加全局连接记录
|
self.active_connections = set() # 添加全局连接记录
|
||||||
|
|
||||||
def _create_processing_instances(self):
|
def _create_processing_instances(self):
|
||||||
memory_cls_name = self.config["selected_module"].get("Memory", "nomem") # 默认使用nomem
|
memory_cls_name = self.config["selected_module"].get(
|
||||||
has_memory_cfg = self.config.get("Memory") and memory_cls_name in self.config["Memory"]
|
"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 {}
|
memory_cfg = self.config["Memory"][memory_cls_name] if has_memory_cfg else {}
|
||||||
|
|
||||||
"""创建处理模块实例"""
|
"""创建处理模块实例"""
|
||||||
return (
|
return (
|
||||||
vad.create_instance(
|
vad.create_instance(
|
||||||
self.config["selected_module"]["VAD"],
|
self.config["selected_module"]["VAD"],
|
||||||
self.config["VAD"][self.config["selected_module"]["VAD"]]
|
self.config["VAD"][self.config["selected_module"]["VAD"]],
|
||||||
),
|
),
|
||||||
asr.create_instance(
|
asr.create_instance(
|
||||||
self.config["selected_module"]["ASR"]
|
(
|
||||||
if not 'type' in self.config["ASR"][self.config["selected_module"]["ASR"]]
|
self.config["selected_module"]["ASR"]
|
||||||
else
|
if not "type"
|
||||||
self.config["ASR"][self.config["selected_module"]["ASR"]]["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["ASR"][self.config["selected_module"]["ASR"]],
|
||||||
self.config["delete_audio"]
|
self.config["delete_audio"],
|
||||||
),
|
),
|
||||||
llm.create_instance(
|
llm.create_instance(
|
||||||
self.config["selected_module"]["LLM"]
|
(
|
||||||
if not 'type' in self.config["LLM"][self.config["selected_module"]["LLM"]]
|
self.config["selected_module"]["LLM"]
|
||||||
else
|
if not "type"
|
||||||
self.config["LLM"][self.config["selected_module"]["LLM"]]['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"]],
|
self.config["LLM"][self.config["selected_module"]["LLM"]],
|
||||||
),
|
),
|
||||||
tts.create_instance(
|
tts.create_instance(
|
||||||
self.config["selected_module"]["TTS"]
|
(
|
||||||
if not 'type' in self.config["TTS"][self.config["selected_module"]["TTS"]]
|
self.config["selected_module"]["TTS"]
|
||||||
else
|
if not "type"
|
||||||
self.config["TTS"][self.config["selected_module"]["TTS"]]["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["TTS"][self.config["selected_module"]["TTS"]],
|
||||||
self.config["delete_audio"]
|
self.config["delete_audio"],
|
||||||
),
|
),
|
||||||
memory.create_instance(memory_cls_name, memory_cfg),
|
memory.create_instance(memory_cls_name, memory_cfg),
|
||||||
intent.create_instance(
|
intent.create_instance(
|
||||||
self.config["selected_module"]["Intent"]
|
(
|
||||||
if not 'type' in self.config["Intent"][self.config["selected_module"]["Intent"]]
|
self.config["selected_module"]["Intent"]
|
||||||
else
|
if not "type"
|
||||||
self.config["Intent"][self.config["selected_module"]["Intent"]]["type"],
|
in self.config["Intent"][self.config["selected_module"]["Intent"]]
|
||||||
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"]],
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -64,22 +86,32 @@ class WebSocketServer:
|
|||||||
host = server_config["ip"]
|
host = server_config["ip"]
|
||||||
port = server_config["port"]
|
port = server_config["port"]
|
||||||
selected_module = self.config.get("selected_module")
|
selected_module = self.config.get("selected_module")
|
||||||
self.logger.bind(tag=TAG).info(f"selected_module values: {', '.join(selected_module.values())}")
|
self.logger.bind(tag=TAG).info(
|
||||||
|
f"selected_module values: {', '.join(selected_module.values())}"
|
||||||
|
)
|
||||||
|
|
||||||
self.logger.bind(tag=TAG).info("Server is running at ws://{}:{}", get_local_ip(), port)
|
self.logger.bind(tag=TAG).info(
|
||||||
self.logger.bind(tag=TAG).info("=======上面的地址是websocket协议地址,请勿用浏览器访问=======")
|
"Server is running at ws://{}:{}", get_local_ip(), port
|
||||||
async with websockets.serve(
|
)
|
||||||
self._handle_connection,
|
self.logger.bind(tag=TAG).info(
|
||||||
host,
|
"=======上面的地址是websocket协议地址,请勿用浏览器访问======="
|
||||||
port
|
)
|
||||||
):
|
async with websockets.serve(self._handle_connection, host, port):
|
||||||
await asyncio.Future()
|
await asyncio.Future()
|
||||||
|
|
||||||
async def _handle_connection(self, websocket):
|
async def _handle_connection(self, websocket):
|
||||||
"""处理新连接,每次创建独立的ConnectionHandler"""
|
"""处理新连接,每次创建独立的ConnectionHandler"""
|
||||||
# 创建ConnectionHandler时传入当前server实例
|
# 创建ConnectionHandler时传入当前server实例
|
||||||
# tts 变成链接的时候创建,避免并非问题
|
# tts 变成链接的时候创建,避免并非问题
|
||||||
handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._memory, self.intent)
|
handler = ConnectionHandler(
|
||||||
|
self.config,
|
||||||
|
self._vad,
|
||||||
|
self._asr,
|
||||||
|
self._llm,
|
||||||
|
self._tts,
|
||||||
|
self._memory,
|
||||||
|
self.intent,
|
||||||
|
)
|
||||||
self.active_connections.add(handler)
|
self.active_connections.add(handler)
|
||||||
try:
|
try:
|
||||||
await handler.handle_connection(websocket)
|
await handler.handle_connection(websocket)
|
||||||
|
|||||||
@@ -28,29 +28,32 @@ play_music_function_desc = {
|
|||||||
"properties": {
|
"properties": {
|
||||||
"song_name": {
|
"song_name": {
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"description": "歌曲名称,如果没有指定具体歌名则为'random'"
|
"description": "歌曲名称,如果没有指定具体歌名则为'random'",
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": ["song_name"]
|
"required": ["song_name"],
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@register_function('play_music', play_music_function_desc, ToolType.SYSTEM_CTL)
|
@register_function("play_music", play_music_function_desc, ToolType.SYSTEM_CTL)
|
||||||
def play_music(conn, song_name: str):
|
def play_music(conn, song_name: str):
|
||||||
try:
|
try:
|
||||||
music_intent = f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
|
music_intent = (
|
||||||
|
f"播放音乐 {song_name}" if song_name != "random" else "随机播放音乐"
|
||||||
|
)
|
||||||
|
|
||||||
# 检查事件循环状态
|
# 检查事件循环状态
|
||||||
if not conn.loop.is_running():
|
if not conn.loop.is_running():
|
||||||
logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
|
logger.bind(tag=TAG).error("事件循环未运行,无法提交任务")
|
||||||
return ActionResponse(action=Action.RESPONSE, result="系统繁忙", response="请稍后再试")
|
return ActionResponse(
|
||||||
|
action=Action.RESPONSE, result="系统繁忙", response="请稍后再试"
|
||||||
|
)
|
||||||
|
|
||||||
# 提交异步任务
|
# 提交异步任务
|
||||||
future = asyncio.run_coroutine_threadsafe(
|
future = asyncio.run_coroutine_threadsafe(
|
||||||
handle_music_command(conn, music_intent),
|
handle_music_command(conn, music_intent), conn.loop
|
||||||
conn.loop
|
|
||||||
)
|
)
|
||||||
|
|
||||||
# 非阻塞回调处理
|
# 非阻塞回调处理
|
||||||
@@ -63,10 +66,14 @@ def play_music(conn, song_name: str):
|
|||||||
|
|
||||||
future.add_done_callback(handle_done)
|
future.add_done_callback(handle_done)
|
||||||
|
|
||||||
return ActionResponse(action=Action.RESPONSE, result="指令已接收", response="正在为您播放音乐")
|
return ActionResponse(
|
||||||
|
action=Action.RESPONSE, result="指令已接收", response="正在为您播放音乐"
|
||||||
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
logger.bind(tag=TAG).error(f"处理音乐意图错误: {e}")
|
||||||
return ActionResponse(action=Action.RESPONSE, result=str(e), response="播放音乐时出错了")
|
return ActionResponse(
|
||||||
|
action=Action.RESPONSE, result=str(e), response="播放音乐时出错了"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _extract_song_name(text):
|
def _extract_song_name(text):
|
||||||
@@ -106,7 +113,9 @@ def get_music_files(music_dir, music_ext):
|
|||||||
if ext in music_ext:
|
if ext in music_ext:
|
||||||
# 添加相对路径
|
# 添加相对路径
|
||||||
music_files.append(str(file.relative_to(music_dir)))
|
music_files.append(str(file.relative_to(music_dir)))
|
||||||
music_file_names.append(os.path.splitext(str(file.relative_to(music_dir)))[0])
|
music_file_names.append(
|
||||||
|
os.path.splitext(str(file.relative_to(music_dir)))[0]
|
||||||
|
)
|
||||||
return music_files, music_file_names
|
return music_files, music_file_names
|
||||||
|
|
||||||
|
|
||||||
@@ -118,15 +127,20 @@ def initialize_music_handler(conn):
|
|||||||
MUSIC_CACHE["music_dir"] = os.path.abspath(
|
MUSIC_CACHE["music_dir"] = os.path.abspath(
|
||||||
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
|
MUSIC_CACHE["music_config"].get("music_dir", "./music") # 默认路径修改
|
||||||
)
|
)
|
||||||
MUSIC_CACHE["music_ext"] = MUSIC_CACHE["music_config"].get("music_ext", (".mp3", ".wav", ".p3"))
|
MUSIC_CACHE["music_ext"] = MUSIC_CACHE["music_config"].get(
|
||||||
MUSIC_CACHE["refresh_time"] = MUSIC_CACHE["music_config"].get("refresh_time", 60)
|
"music_ext", (".mp3", ".wav", ".p3")
|
||||||
|
)
|
||||||
|
MUSIC_CACHE["refresh_time"] = MUSIC_CACHE["music_config"].get(
|
||||||
|
"refresh_time", 60
|
||||||
|
)
|
||||||
else:
|
else:
|
||||||
MUSIC_CACHE["music_dir"] = os.path.abspath("./music")
|
MUSIC_CACHE["music_dir"] = os.path.abspath("./music")
|
||||||
MUSIC_CACHE["music_ext"] = (".mp3", ".wav", ".p3")
|
MUSIC_CACHE["music_ext"] = (".mp3", ".wav", ".p3")
|
||||||
MUSIC_CACHE["refresh_time"] = 60
|
MUSIC_CACHE["refresh_time"] = 60
|
||||||
# 获取音乐文件列表
|
# 获取音乐文件列表
|
||||||
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(MUSIC_CACHE["music_dir"],
|
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(
|
||||||
MUSIC_CACHE["music_ext"])
|
MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"]
|
||||||
|
)
|
||||||
MUSIC_CACHE["scan_time"] = time.time()
|
MUSIC_CACHE["scan_time"] = time.time()
|
||||||
return MUSIC_CACHE
|
return MUSIC_CACHE
|
||||||
|
|
||||||
@@ -136,15 +150,16 @@ async def handle_music_command(conn, text):
|
|||||||
global MUSIC_CACHE
|
global MUSIC_CACHE
|
||||||
|
|
||||||
"""处理音乐播放指令"""
|
"""处理音乐播放指令"""
|
||||||
clean_text = re.sub(r'[^\w\s]', '', text).strip()
|
clean_text = re.sub(r"[^\w\s]", "", text).strip()
|
||||||
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
|
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
|
||||||
|
|
||||||
# 尝试匹配具体歌名
|
# 尝试匹配具体歌名
|
||||||
if os.path.exists(MUSIC_CACHE["music_dir"]):
|
if os.path.exists(MUSIC_CACHE["music_dir"]):
|
||||||
if time.time() - MUSIC_CACHE["scan_time"] > MUSIC_CACHE["refresh_time"]:
|
if time.time() - MUSIC_CACHE["scan_time"] > MUSIC_CACHE["refresh_time"]:
|
||||||
# 刷新音乐文件列表
|
# 刷新音乐文件列表
|
||||||
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = get_music_files(MUSIC_CACHE["music_dir"],
|
MUSIC_CACHE["music_files"], MUSIC_CACHE["music_file_names"] = (
|
||||||
MUSIC_CACHE["music_ext"])
|
get_music_files(MUSIC_CACHE["music_dir"], MUSIC_CACHE["music_ext"])
|
||||||
|
)
|
||||||
MUSIC_CACHE["scan_time"] = time.time()
|
MUSIC_CACHE["scan_time"] = time.time()
|
||||||
|
|
||||||
potential_song = _extract_song_name(clean_text)
|
potential_song = _extract_song_name(clean_text)
|
||||||
@@ -192,8 +207,12 @@ async def play_local_music(conn, specific_file=None):
|
|||||||
opus_packets, duration = conn.tts.audio_to_opus_data(music_path)
|
opus_packets, duration = conn.tts.audio_to_opus_data(music_path)
|
||||||
conn.tts.tts_audio_queue.put(
|
conn.tts.tts_audio_queue.put(
|
||||||
TTSMessageDTO(
|
TTSMessageDTO(
|
||||||
u_id=conn.u_id, msg_type=MsgType.TTS_TEXT_RESPONSE, content=opus_packets,
|
u_id=conn.u_id,
|
||||||
tts_finish_text="", sentence_type=None, duration=0
|
msg_type=MsgType.TTS_TEXT_RESPONSE,
|
||||||
|
content=opus_packets,
|
||||||
|
tts_finish_text="",
|
||||||
|
sentence_type=None,
|
||||||
|
duration=0,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user