update:格式化代码

This commit is contained in:
hrz
2025-04-01 14:27:17 +08:00
7 changed files with 507 additions and 222 deletions
+178 -57
View File
@@ -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,7 +90,6 @@ 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)
@@ -86,13 +98,15 @@ class TTSProvider(TTSProviderBase):
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)
+113 -44
View 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)
+22 -6
View File
@@ -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,
)
+64 -32
View File
@@ -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,
) )
) )