mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-25 00:23:53 +08:00
fix: 播放咋音问题
This commit is contained in:
@@ -79,7 +79,7 @@ class ConnectionHandler:
|
||||
self.intent = _intent
|
||||
|
||||
# vad相关变量
|
||||
self.client_audio_buffer = bytes()
|
||||
self.client_audio_buffer = bytearray()
|
||||
self.client_have_voice = False
|
||||
self.client_have_voice_last_time = 0.0
|
||||
self.client_no_voice_last_time = 0.0
|
||||
@@ -139,9 +139,6 @@ class ConnectionHandler:
|
||||
await self.websocket.send(json.dumps(self.welcome_msg))
|
||||
# Load private configuration if device_id is provided
|
||||
bUsePrivateConfig = self.config.get("use_private_config", False)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"bUsePrivateConfig: {bUsePrivateConfig}, device_id: {device_id}"
|
||||
)
|
||||
if bUsePrivateConfig and device_id:
|
||||
try:
|
||||
self.private_config = PrivateConfig(
|
||||
@@ -225,50 +222,58 @@ class ConnectionHandler:
|
||||
self.prompt = self.private_config.private_config.get("prompt", self.prompt)
|
||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||
|
||||
"""加载插件"""
|
||||
self.func_handler = FunctionHandler(self)
|
||||
self.mcp_manager = MCPManager(self)
|
||||
"""加载记忆"""
|
||||
device_id = self.headers.get("device-id", None)
|
||||
self.memory.init_memory(device_id, self.llm)
|
||||
|
||||
"""为意图识别设置LLM,优先使用专用LLM"""
|
||||
# 检查是否配置了专用的意图识别LLM
|
||||
intent_llm_name = self.config["Intent"]["intent_llm"]["llm"]
|
||||
|
||||
# 记录开始初始化意图识别LLM的时间
|
||||
intent_llm_init_start = time.time()
|
||||
|
||||
if (
|
||||
not self.use_function_call_mode
|
||||
and intent_llm_name
|
||||
and intent_llm_name in self.config["LLM"]
|
||||
):
|
||||
# 如果配置了专用LLM,则创建独立的LLM实例
|
||||
from core.utils import llm as llm_utils
|
||||
|
||||
intent_llm_config = self.config["LLM"][intent_llm_name]
|
||||
intent_llm_type = intent_llm_config.get("type", intent_llm_name)
|
||||
intent_llm = llm_utils.create_instance(intent_llm_type, intent_llm_config)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}"
|
||||
)
|
||||
|
||||
self.intent.set_llm(intent_llm)
|
||||
else:
|
||||
# 否则使用主LLM
|
||||
self.intent.set_llm(self.llm)
|
||||
|
||||
# 记录意图识别LLM初始化耗时
|
||||
intent_llm_init_time = time.time() - intent_llm_init_start
|
||||
|
||||
self._initialize_memory()
|
||||
"""加载意图识别"""
|
||||
self._initialize_intent()
|
||||
"""加载位置信息"""
|
||||
self.client_ip_info = get_ip_info(self.client_ip)
|
||||
if self.client_ip_info is not None and "city" in self.client_ip_info:
|
||||
self.logger.bind(tag=TAG).info(f"Client ip info: {self.client_ip_info}")
|
||||
self.prompt = self.prompt + f"\nuser location:{self.client_ip_info}"
|
||||
|
||||
self.dialogue.update_system_message(self.prompt)
|
||||
|
||||
def _initialize_memory(self):
|
||||
"""初始化记忆模块"""
|
||||
device_id = self.headers.get("device-id", None)
|
||||
self.memory.init_memory(device_id, self.llm)
|
||||
|
||||
def _initialize_intent(self):
|
||||
"""初始化意图识别模块"""
|
||||
# 获取意图识别配置
|
||||
intent_config = self.config["Intent"]
|
||||
intent_type = self.config["selected_module"]["Intent"]
|
||||
|
||||
# 如果使用 nointent,直接返回
|
||||
if intent_type == "nointent":
|
||||
return
|
||||
# 使用 intent_llm 模式
|
||||
elif intent_type == "intent_llm":
|
||||
intent_llm_name = intent_config["intent_llm"]["llm"]
|
||||
|
||||
if intent_llm_name and intent_llm_name in self.config["LLM"]:
|
||||
# 如果配置了专用LLM,则创建独立的LLM实例
|
||||
from core.utils import llm as llm_utils
|
||||
|
||||
intent_llm_config = self.config["LLM"][intent_llm_name]
|
||||
intent_llm_type = intent_llm_config.get("type", intent_llm_name)
|
||||
intent_llm = llm_utils.create_instance(
|
||||
intent_llm_type, intent_llm_config
|
||||
)
|
||||
self.logger.bind(tag=TAG).info(
|
||||
f"为意图识别创建了专用LLM: {intent_llm_name}, 类型: {intent_llm_type}"
|
||||
)
|
||||
self.intent.set_llm(intent_llm)
|
||||
else:
|
||||
# 否则使用主LLM
|
||||
self.intent.set_llm(self.llm)
|
||||
self.logger.bind(tag=TAG).info("使用主LLM作为意图识别模型")
|
||||
|
||||
"""加载插件"""
|
||||
self.func_handler = FunctionHandler(self)
|
||||
self.mcp_manager = MCPManager(self)
|
||||
|
||||
"""加载MCP工具"""
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.mcp_manager.initialize_servers(), self.loop
|
||||
@@ -652,7 +657,8 @@ class ConnectionHandler:
|
||||
async def close(self, ws=None):
|
||||
"""资源清理方法"""
|
||||
# 清理MCP资源
|
||||
await self.mcp_manager.cleanup_all()
|
||||
if hasattr(self, "mcp_manager") and self.mcp_manager:
|
||||
await self.mcp_manager.cleanup_all()
|
||||
|
||||
# 触发停止事件并清理资源
|
||||
if self.stop_event:
|
||||
@@ -688,7 +694,7 @@ class ConnectionHandler:
|
||||
# q.queue.put(None)
|
||||
|
||||
def reset_vad_states(self):
|
||||
self.client_audio_buffer = bytes()
|
||||
self.client_audio_buffer = bytearray()
|
||||
self.client_have_voice = False
|
||||
self.client_have_voice_last_time = 0
|
||||
self.client_voice_stop = False
|
||||
|
||||
@@ -69,12 +69,13 @@ async def process_intent_result(conn, intent_result, original_text):
|
||||
# 检查是否有function_call
|
||||
if "function_call" in intent_data:
|
||||
# 直接从意图识别获取了function_call
|
||||
logger.bind(tag=TAG).info(
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"检测到function_call格式的意图结果: {intent_data['function_call']['name']}"
|
||||
)
|
||||
function_name = intent_data["function_call"]["name"]
|
||||
if function_name == "continue_chat":
|
||||
return False
|
||||
|
||||
function_args = None
|
||||
if "arguments" in intent_data["function_call"]:
|
||||
function_args = intent_data["function_call"]["arguments"]
|
||||
|
||||
@@ -50,6 +50,12 @@ def create_iot_function(device_name, method_name, method_info):
|
||||
conn, response_success=None, response_failure=None, **params
|
||||
):
|
||||
try:
|
||||
# 设置默认响应消息
|
||||
if not response_success:
|
||||
response_success = "操作成功"
|
||||
if not response_failure:
|
||||
response_failure = "操作失败"
|
||||
|
||||
# 打印响应参数
|
||||
logger.bind(tag=TAG).info(
|
||||
f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'"
|
||||
@@ -263,6 +269,8 @@ def register_device_type(descriptor):
|
||||
|
||||
# 用于接受前端设备推送的搜索iot描述
|
||||
async def handleIotDescriptors(conn, descriptors):
|
||||
if not conn.use_function_call_mode:
|
||||
return
|
||||
wait_max_time = 5
|
||||
while conn.func_handler is None or not conn.func_handler.finish_init:
|
||||
await asyncio.sleep(1)
|
||||
|
||||
@@ -2,14 +2,16 @@
|
||||
Opus编码工具类
|
||||
将PCM音频数据编码为Opus格式
|
||||
"""
|
||||
|
||||
import array
|
||||
import logging
|
||||
import traceback
|
||||
|
||||
import numpy as np
|
||||
from typing import List, Optional
|
||||
from opuslib import Encoder
|
||||
from opuslib import constants
|
||||
from opuslib_next import Encoder
|
||||
from opuslib_next import constants
|
||||
|
||||
|
||||
class OpusEncoderUtils:
|
||||
"""PCM到Opus的编码器"""
|
||||
@@ -17,7 +19,7 @@ class OpusEncoderUtils:
|
||||
def __init__(self, sample_rate: int, channels: int, frame_size_ms: int):
|
||||
"""
|
||||
初始化Opus编码器
|
||||
|
||||
|
||||
Args:
|
||||
sample_rate: 采样率 (Hz)
|
||||
channels: 通道数 (1=单声道, 2=立体声)
|
||||
@@ -30,20 +32,18 @@ class OpusEncoderUtils:
|
||||
self.frame_size = (sample_rate * frame_size_ms) // 1000
|
||||
# 总帧大小 = 每帧样本数 * 通道数
|
||||
self.total_frame_size = self.frame_size * channels
|
||||
|
||||
|
||||
# 比特率和复杂度设置
|
||||
self.bitrate = 24000 # bps
|
||||
self.complexity = 10 # 最高质量
|
||||
|
||||
|
||||
# 缓冲区初始化为空
|
||||
self.buffer = np.array([], dtype=np.int16)
|
||||
|
||||
|
||||
try:
|
||||
# 创建Opus编码器
|
||||
self.encoder = Encoder(
|
||||
sample_rate,
|
||||
channels,
|
||||
constants.APPLICATION_AUDIO # 音频优化模式
|
||||
sample_rate, channels, constants.APPLICATION_AUDIO # 音频优化模式
|
||||
)
|
||||
self.encoder.bitrate = self.bitrate
|
||||
self.encoder.complexity = self.complexity
|
||||
@@ -60,48 +60,48 @@ class OpusEncoderUtils:
|
||||
def encode_pcm_to_opus(self, pcm_data: bytes, end_of_stream: bool) -> List[bytes]:
|
||||
"""
|
||||
将PCM数据编码为Opus格式
|
||||
|
||||
|
||||
Args:
|
||||
pcm_data: PCM字节数据
|
||||
end_of_stream: 是否为流的结束
|
||||
|
||||
|
||||
Returns:
|
||||
Opus数据包列表
|
||||
"""
|
||||
# 将字节数据转换为short数组
|
||||
new_samples = self._convert_bytes_to_shorts(pcm_data)
|
||||
|
||||
|
||||
# 校验PCM数据
|
||||
self._validate_pcm_data(new_samples)
|
||||
|
||||
|
||||
# 将新数据追加到缓冲区
|
||||
self.buffer = np.append(self.buffer, new_samples)
|
||||
|
||||
|
||||
opus_packets = []
|
||||
offset = 0
|
||||
|
||||
|
||||
# 处理所有完整帧
|
||||
while offset <= len(self.buffer) - self.total_frame_size:
|
||||
frame = self.buffer[offset:offset + self.total_frame_size]
|
||||
frame = self.buffer[offset : offset + self.total_frame_size]
|
||||
output = self._encode(frame)
|
||||
if output:
|
||||
opus_packets.append(output)
|
||||
offset += self.total_frame_size
|
||||
|
||||
|
||||
# 保留未处理的样本
|
||||
self.buffer = self.buffer[offset:]
|
||||
|
||||
|
||||
# 流结束时处理剩余数据
|
||||
if end_of_stream and len(self.buffer) > 0:
|
||||
# 创建最后一帧并用0填充
|
||||
last_frame = np.zeros(self.total_frame_size, dtype=np.int16)
|
||||
last_frame[:len(self.buffer)] = self.buffer
|
||||
|
||||
last_frame[: len(self.buffer)] = self.buffer
|
||||
|
||||
output = self._encode(last_frame)
|
||||
if output:
|
||||
opus_packets.append(output)
|
||||
self.buffer = np.array([], dtype=np.int16)
|
||||
|
||||
|
||||
return opus_packets
|
||||
|
||||
def _encode(self, frame: np.ndarray) -> Optional[bytes]:
|
||||
@@ -134,4 +134,4 @@ class OpusEncoderUtils:
|
||||
def close(self):
|
||||
"""关闭编码器并释放资源"""
|
||||
# opuslib没有明确的关闭方法,Python的垃圾回收会处理
|
||||
pass
|
||||
pass
|
||||
|
||||
@@ -9,20 +9,23 @@ logger = setup_logging()
|
||||
class IntentProviderBase(ABC):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.intent_options = config.get("intent_options", {
|
||||
"handle_exit_intent": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
||||
"play_music": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
|
||||
"get_weather": "查询天气, 用户希望查询某个地点的天气情况",
|
||||
"get_news": "查询新闻, 用户希望查询最新新闻或特定类型的新闻",
|
||||
"get_lunar": "用于获取今天的阴历/农历和黄历信息",
|
||||
"get_time": "获取今天日期或者当前时间信息",
|
||||
"continue_chat": "继续聊天",
|
||||
})
|
||||
self.intent_options = [
|
||||
{
|
||||
"name": "handle_exit_intent",
|
||||
"desc": "结束聊天, 用户发来如再见之类的表示结束的话, 不想再进行对话的时候",
|
||||
},
|
||||
{
|
||||
"name": "play_music",
|
||||
"desc": "播放音乐, 用户希望你可以播放音乐, 只用于播放音乐的意图",
|
||||
},
|
||||
{"name": "get_time", "desc": "获取今天日期或者当前时间信息"},
|
||||
{"name": "continue_chat", "desc": "继续聊天"},
|
||||
]
|
||||
|
||||
def set_llm(self, llm):
|
||||
self.llm = llm
|
||||
# 获取模型名称和类型信息
|
||||
model_name = getattr(llm, 'model_name', str(llm.__class__.__name__))
|
||||
model_name = getattr(llm, "model_name", str(llm.__class__.__name__))
|
||||
# 记录更详细的日志
|
||||
logger.bind(tag=TAG).info(f"意图识别设置LLM: {model_name}")
|
||||
|
||||
@@ -35,7 +38,7 @@ class IntentProviderBase(ABC):
|
||||
Returns:
|
||||
返回识别出的意图,格式为:
|
||||
- "继续聊天"
|
||||
- "结束聊天"
|
||||
- "结束聊天"
|
||||
- "播放音乐 歌名" 或 "随机播放音乐"
|
||||
- "查询天气 地点名" 或 "查询天气 [当前位置]"
|
||||
"""
|
||||
|
||||
@@ -16,5 +16,7 @@ class IntentProvider(IntentProviderBase):
|
||||
Returns:
|
||||
固定返回"继续聊天"
|
||||
"""
|
||||
logger.bind(tag=TAG).debug("Using functionCallProvider, always returning continue chat")
|
||||
return self.intent_options["continue_chat"]
|
||||
logger.bind(tag=TAG).debug(
|
||||
"Using functionCallProvider, always returning continue chat"
|
||||
)
|
||||
return '{"function_call": {"name": "continue_chat"}}'
|
||||
|
||||
@@ -27,119 +27,94 @@ class IntentProvider(IntentProviderBase):
|
||||
Returns:
|
||||
格式化后的系统提示词
|
||||
"""
|
||||
intent_list = []
|
||||
|
||||
prompt = (
|
||||
"你是一个意图识别助手。请分析用户的最后一句话,判断用户意图属于以下哪一类:\n"
|
||||
"<start>"
|
||||
f"{', '.join(intent_list)}"
|
||||
f"{str(self.intent_options)}"
|
||||
"<end>\n"
|
||||
"处理步骤:"
|
||||
"1. 思考意图类型,生成function_call格式"
|
||||
"\n\n"
|
||||
"返回格式示例:\n"
|
||||
"1. 播放音乐意图: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"音乐名称\"}}}\n"
|
||||
"2. 查询天气意图: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"地点名称\", \"lang\": \"zh_CN\"}}}\n"
|
||||
"3. 查询新闻意图: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"新闻类别\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||
"4. 结束对话意图: {\"function_call\": {\"name\": \"handle_exit_intent\", \"arguments\": {\"say_goodbye\": \"goodbye\"}}}\n"
|
||||
"5. 获取当天日期时间: {\"function_call\": {\"name\": \"get_time\"}}\n"
|
||||
"6. 获取当前黄历意图: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
|
||||
"7. 继续聊天意图: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||
'1. 播放音乐意图: {"function_call": {"name": "play_music", "arguments": {"song_name": "音乐名称"}}}\n'
|
||||
'2. 结束对话意图: {"function_call": {"name": "handle_exit_intent", "arguments": {"say_goodbye": "goodbye"}}}\n'
|
||||
'3. 获取当天日期时间: {"function_call": {"name": "get_time"}}\n'
|
||||
'4. 继续聊天意图: {"function_call": {"name": "continue_chat"}}\n'
|
||||
"\n"
|
||||
"注意:\n"
|
||||
"- 播放音乐:无歌名时,song_name设为\"random\"\n"
|
||||
"- 查询天气:无地点时,location设为null\n"
|
||||
"- 查询新闻:无类别时,category设为null;查询详情时,detail设为true\n"
|
||||
'- 播放音乐:无歌名时,song_name设为"random"\n'
|
||||
"- 如果没有明显的意图,应按照继续聊天意图处理\n"
|
||||
"- 只返回纯JSON,不要任何其他内容\n"
|
||||
"\n"
|
||||
"示例分析:\n"
|
||||
"```\n"
|
||||
"用户: 你好小智\n"
|
||||
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 你今天怎么样?\n"
|
||||
"返回: {\"function_call\": {\"name\": \"continue_chat\"}}\n"
|
||||
"用户: 你也太搞笑了\n"
|
||||
'返回: {"function_call": {"name": "continue_chat"}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 现在是几号了?现在几点了?\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_time\"}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 今天农历是多少?\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_lunar\"}}\n"
|
||||
'返回: {"function_call": {"name": "get_time"}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 我们明天再聊吧\n"
|
||||
"返回: {\"function_call\": {\"name\": \"handle_exit_intent\"}}\n"
|
||||
'返回: {"function_call": {"name": "handle_exit_intent"}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 播放中秋月\n"
|
||||
"返回: {\"function_call\": {\"name\": \"play_music\", \"arguments\": {\"song_name\": \"中秋月\"}}}\n"
|
||||
'返回: {"function_call": {"name": "play_music", "arguments": {"song_name": "中秋月"}}}\n'
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 北京天气怎么样\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": \"北京\", \"lang\": \"zh_CN\"}}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 今天天气怎么样\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_weather\", \"arguments\": {\"location\": null, \"lang\": \"zh_CN\"}}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 播报财经新闻\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": \"财经\", \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 有什么最新新闻\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"category\": null, \"detail\": false, \"lang\": \"zh_CN\"}}}\n"
|
||||
"```\n"
|
||||
"```\n"
|
||||
"用户: 详细介绍一下这条新闻\n"
|
||||
"返回: {\"function_call\": {\"name\": \"get_news\", \"arguments\": {\"detail\": true, \"lang\": \"zh_CN\"}}}\n"
|
||||
"```\n"
|
||||
"可用的音乐名称:\n"
|
||||
)
|
||||
return prompt
|
||||
|
||||
|
||||
def clean_cache(self):
|
||||
"""清理过期缓存"""
|
||||
now = time.time()
|
||||
# 找出过期键
|
||||
expired_keys = [k for k, v in self.intent_cache.items() if now - v['timestamp'] > self.cache_expiry]
|
||||
expired_keys = [
|
||||
k
|
||||
for k, v in self.intent_cache.items()
|
||||
if now - v["timestamp"] > self.cache_expiry
|
||||
]
|
||||
for key in expired_keys:
|
||||
del self.intent_cache[key]
|
||||
|
||||
|
||||
# 如果缓存太大,移除最旧的条目
|
||||
if len(self.intent_cache) > self.cache_max_size:
|
||||
# 按时间戳排序并保留最新的条目
|
||||
sorted_items = sorted(self.intent_cache.items(), key=lambda x: x[1]['timestamp'])
|
||||
for key, _ in sorted_items[:len(sorted_items) - self.cache_max_size]:
|
||||
sorted_items = sorted(
|
||||
self.intent_cache.items(), key=lambda x: x[1]["timestamp"]
|
||||
)
|
||||
for key, _ in sorted_items[: len(sorted_items) - self.cache_max_size]:
|
||||
del self.intent_cache[key]
|
||||
|
||||
async def detect_intent(self, conn, dialogue_history: List[Dict], text: str) -> str:
|
||||
if not self.llm:
|
||||
raise ValueError("LLM provider not set")
|
||||
|
||||
|
||||
# 记录整体开始时间
|
||||
total_start_time = time.time()
|
||||
|
||||
|
||||
# 打印使用的模型信息
|
||||
model_info = getattr(self.llm, 'model_name', str(self.llm.__class__.__name__))
|
||||
logger.bind(tag=TAG).info(f"使用意图识别模型: {model_info}")
|
||||
|
||||
model_info = getattr(self.llm, "model_name", str(self.llm.__class__.__name__))
|
||||
logger.bind(tag=TAG).debug(f"使用意图识别模型: {model_info}")
|
||||
|
||||
# 计算缓存键
|
||||
cache_key = hashlib.md5(text.encode()).hexdigest()
|
||||
|
||||
|
||||
# 检查缓存
|
||||
if cache_key in self.intent_cache:
|
||||
cache_entry = self.intent_cache[cache_key]
|
||||
# 检查缓存是否过期
|
||||
if time.time() - cache_entry['timestamp'] <= self.cache_expiry:
|
||||
if time.time() - cache_entry["timestamp"] <= self.cache_expiry:
|
||||
cache_time = time.time() - total_start_time
|
||||
logger.bind(tag=TAG).info(f"使用缓存的意图: {cache_key} -> {cache_entry['intent']}, 耗时: {cache_time:.4f}秒")
|
||||
return cache_entry['intent']
|
||||
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"使用缓存的意图: {cache_key} -> {cache_entry['intent']}, 耗时: {cache_time:.4f}秒"
|
||||
)
|
||||
return cache_entry["intent"]
|
||||
|
||||
# 清理缓存
|
||||
self.clean_cache()
|
||||
|
||||
@@ -158,38 +133,41 @@ class IntentProvider(IntentProviderBase):
|
||||
music_file_names = music_config["music_file_names"]
|
||||
prompt_music = f"{self.promot}\n<start>{music_file_names}\n<end>"
|
||||
logger.bind(tag=TAG).debug(f"User prompt: {prompt_music}")
|
||||
|
||||
|
||||
# 记录预处理完成时间
|
||||
preprocess_time = time.time() - total_start_time
|
||||
logger.bind(tag=TAG).debug(f"意图识别预处理耗时: {preprocess_time:.4f}秒")
|
||||
|
||||
|
||||
# 使用LLM进行意图识别
|
||||
llm_start_time = time.time()
|
||||
logger.bind(tag=TAG).info(f"开始LLM意图识别调用, 模型: {model_info}")
|
||||
|
||||
logger.bind(tag=TAG).debug(f"开始LLM意图识别调用, 模型: {model_info}")
|
||||
|
||||
intent = self.llm.response_no_stream(
|
||||
system_prompt=prompt_music,
|
||||
user_prompt=user_prompt
|
||||
system_prompt=prompt_music, user_prompt=user_prompt
|
||||
)
|
||||
|
||||
|
||||
# 记录LLM调用完成时间
|
||||
llm_time = time.time() - llm_start_time
|
||||
logger.bind(tag=TAG).info(f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}秒")
|
||||
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"LLM意图识别完成, 模型: {model_info}, 调用耗时: {llm_time:.4f}秒"
|
||||
)
|
||||
|
||||
# 记录后处理开始时间
|
||||
postprocess_start_time = time.time()
|
||||
|
||||
|
||||
# 清理和解析响应
|
||||
intent = intent.strip()
|
||||
# 尝试提取JSON部分
|
||||
match = re.search(r'\{.*\}', intent, re.DOTALL)
|
||||
match = re.search(r"\{.*\}", intent, re.DOTALL)
|
||||
if match:
|
||||
intent = match.group(0)
|
||||
|
||||
|
||||
# 记录总处理时间
|
||||
total_time = time.time() - total_start_time
|
||||
logger.bind(tag=TAG).info(f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'")
|
||||
|
||||
logger.bind(tag=TAG).debug(
|
||||
f"【意图识别性能】模型: {model_info}, 总耗时: {total_time:.4f}秒, LLM调用: {llm_time:.4f}秒, 查询: '{text[:20]}...'"
|
||||
)
|
||||
|
||||
# 尝试解析为JSON
|
||||
try:
|
||||
intent_data = json.loads(intent)
|
||||
@@ -198,38 +176,42 @@ class IntentProvider(IntentProviderBase):
|
||||
function_data = intent_data["function_call"]
|
||||
function_name = function_data.get("name")
|
||||
function_args = function_data.get("arguments", {})
|
||||
|
||||
|
||||
# 记录识别到的function call
|
||||
logger.bind(tag=TAG).info(f"识别到function call: {function_name}, 参数: {function_args}")
|
||||
|
||||
logger.bind(tag=TAG).info(
|
||||
f"识别到function call: {function_name}, 参数: {function_args}"
|
||||
)
|
||||
|
||||
# 添加到缓存
|
||||
self.intent_cache[cache_key] = {
|
||||
'intent': intent,
|
||||
'timestamp': time.time()
|
||||
"intent": intent,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
|
||||
# 后处理时间
|
||||
postprocess_time = time.time() - postprocess_start_time
|
||||
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒")
|
||||
|
||||
|
||||
# 确保返回完全序列化的JSON字符串
|
||||
return intent
|
||||
else:
|
||||
# 添加到缓存
|
||||
self.intent_cache[cache_key] = {
|
||||
'intent': intent,
|
||||
'timestamp': time.time()
|
||||
"intent": intent,
|
||||
"timestamp": time.time(),
|
||||
}
|
||||
|
||||
|
||||
# 后处理时间
|
||||
postprocess_time = time.time() - postprocess_start_time
|
||||
logger.bind(tag=TAG).debug(f"意图后处理耗时: {postprocess_time:.4f}秒")
|
||||
|
||||
|
||||
# 返回普通意图
|
||||
return intent
|
||||
except json.JSONDecodeError:
|
||||
# 后处理时间
|
||||
postprocess_time = time.time() - postprocess_start_time
|
||||
logger.bind(tag=TAG).error(f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}秒")
|
||||
logger.bind(tag=TAG).error(
|
||||
f"无法解析意图JSON: {intent}, 后处理耗时: {postprocess_time:.4f}秒"
|
||||
)
|
||||
# 如果解析失败,默认返回继续聊天意图
|
||||
return "{\"intent\": \"继续聊天\"}"
|
||||
return '{"intent": "继续聊天"}'
|
||||
|
||||
@@ -16,5 +16,7 @@ class IntentProvider(IntentProviderBase):
|
||||
Returns:
|
||||
固定返回"继续聊天"
|
||||
"""
|
||||
logger.bind(tag=TAG).debug("Using NoIntentProvider, always returning continue chat")
|
||||
return self.intent_options["continue_chat"]
|
||||
logger.bind(tag=TAG).debug(
|
||||
"Using NoIntentProvider, always returning continue chat"
|
||||
)
|
||||
return '{"function_call": {"name": "continue_chat"}}'
|
||||
|
||||
@@ -53,6 +53,7 @@ class TTSProviderBase(ABC):
|
||||
",",
|
||||
)
|
||||
self.tts_request = False
|
||||
self.tts_stop_request = False
|
||||
self.processed_chars = 0
|
||||
self.stream = False
|
||||
self.last_to_opus_raw = b""
|
||||
@@ -93,6 +94,9 @@ class TTSProviderBase(ABC):
|
||||
)
|
||||
self.processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
||||
return segment_text
|
||||
elif self.tts_stop_request and current_text:
|
||||
segment_text = current_text
|
||||
return segment_text
|
||||
else:
|
||||
return None
|
||||
|
||||
@@ -115,8 +119,11 @@ class TTSProviderBase(ABC):
|
||||
|
||||
def tts_one_sentence(self, conn, text, u_id=None):
|
||||
if not u_id:
|
||||
u_id = str(uuid.uuid4()).replace("-", "")
|
||||
conn.u_id = u_id
|
||||
if conn.u_id:
|
||||
u_id = conn.u_id
|
||||
else:
|
||||
u_id = str(uuid.uuid4()).replace("-", "")
|
||||
conn.u_id = u_id
|
||||
self.tts_text_queue.put(
|
||||
TTSMessageDTO(u_id=u_id, msg_type=MsgType.START_TTS_REQUEST, content="")
|
||||
)
|
||||
@@ -135,6 +142,7 @@ class TTSProviderBase(ABC):
|
||||
if msg_type == MsgType.START_TTS_REQUEST:
|
||||
# 开始传输tts文本
|
||||
self.tts_request = True
|
||||
self.tts_stop_request = False
|
||||
self.u_id = ttsMessageDTO.u_id
|
||||
# 开启session
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
@@ -152,6 +160,7 @@ class TTSProviderBase(ABC):
|
||||
future.result()
|
||||
elif msg_type == MsgType.STOP_TTS_REQUEST:
|
||||
self.tts_request = False
|
||||
self.tts_stop_request = True
|
||||
future = asyncio.run_coroutine_threadsafe(
|
||||
self.finish_session(ttsMessageDTO.u_id), loop=self.loop
|
||||
)
|
||||
@@ -183,6 +192,7 @@ class TTSProviderBase(ABC):
|
||||
if msg_type == MsgType.START_TTS_REQUEST:
|
||||
# 开始传输tts文本
|
||||
self.tts_request = True
|
||||
self.tts_stop_request = False
|
||||
self.processed_chars = 0
|
||||
self.tts_text_buff = []
|
||||
elif self.tts_request and msg_type == MsgType.TTS_TEXT_REQUEST:
|
||||
@@ -190,6 +200,7 @@ class TTSProviderBase(ABC):
|
||||
elif msg_type == MsgType.STOP_TTS_REQUEST:
|
||||
# 结束传输tts文本,处理最尾巴的数据
|
||||
self.tts_request = False
|
||||
self.tts_stop_request = True
|
||||
segment_text = self._get_segment_text()
|
||||
if segment_text:
|
||||
# 修改部分:创建协程对象
|
||||
|
||||
@@ -10,7 +10,10 @@ import requests
|
||||
|
||||
def get_project_dir():
|
||||
"""获取项目根目录"""
|
||||
return os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + '/'
|
||||
return (
|
||||
os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
+ "/"
|
||||
)
|
||||
|
||||
|
||||
def get_local_ip():
|
||||
@@ -24,6 +27,7 @@ def get_local_ip():
|
||||
except Exception as e:
|
||||
return "127.0.0.1"
|
||||
|
||||
|
||||
def is_private_ip(ip_addr):
|
||||
"""
|
||||
Check if an IP address is a private IP address (compatible with IPv4 and IPv6).
|
||||
@@ -33,46 +37,48 @@ def is_private_ip(ip_addr):
|
||||
"""
|
||||
try:
|
||||
# Validate IPv4 or IPv6 address format
|
||||
if not re.match(r"^(\d{1,3}\.){3}\d{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", ip_addr):
|
||||
if not re.match(
|
||||
r"^(\d{1,3}\.){3}\d{1,3}$|^([0-9a-fA-F]{1,4}:){7}[0-9a-fA-F]{1,4}$", ip_addr
|
||||
):
|
||||
return False # Invalid IP address format
|
||||
|
||||
# IPv4 private address ranges
|
||||
if '.' in ip_addr: # IPv4 address
|
||||
ip_parts = list(map(int, ip_addr.split('.')))
|
||||
if "." in ip_addr: # IPv4 address
|
||||
ip_parts = list(map(int, ip_addr.split(".")))
|
||||
if ip_parts[0] == 10:
|
||||
return True # 10.0.0.0/8 range
|
||||
elif ip_parts[0] == 172 and 16 <= ip_parts[1] <= 31:
|
||||
return True # 172.16.0.0/12 range
|
||||
elif ip_parts[0] == 192 and ip_parts[1] == 168:
|
||||
return True # 192.168.0.0/16 range
|
||||
elif ip_addr == '127.0.0.1':
|
||||
elif ip_addr == "127.0.0.1":
|
||||
return True # Loopback address
|
||||
elif ip_parts[0] == 169 and ip_parts[1] == 254:
|
||||
return True # Link-local address 169.254.0.0/16
|
||||
return True # Link-local address 169.254.0.0/16
|
||||
else:
|
||||
return False # Not a private IPv4 address
|
||||
else: # IPv6 address
|
||||
ip_addr = ip_addr.lower()
|
||||
if ip_addr.startswith('fc00:') or ip_addr.startswith('fd00:'):
|
||||
if ip_addr.startswith("fc00:") or ip_addr.startswith("fd00:"):
|
||||
return True # Unique Local Addresses (FC00::/7)
|
||||
elif ip_addr == '::1':
|
||||
elif ip_addr == "::1":
|
||||
return True # Loopback address
|
||||
elif ip_addr.startswith('fe80:'):
|
||||
return True # Link-local unicast addresses (FE80::/10)
|
||||
elif ip_addr.startswith("fe80:"):
|
||||
return True # Link-local unicast addresses (FE80::/10)
|
||||
else:
|
||||
return False # Not a private IPv6 address
|
||||
|
||||
except (ValueError, IndexError):
|
||||
return False # IP address format error or insufficient segments
|
||||
|
||||
|
||||
def get_ip_info(ip_addr):
|
||||
try:
|
||||
url = "https://whois.pconline.com.cn/ipJson.jsp?json=true"
|
||||
if is_private_ip(ip_addr):
|
||||
ip_addr = ""
|
||||
url = f"https://whois.pconline.com.cn/ipJson.jsp?json=true&ip={ip_addr}"
|
||||
resp = requests.get(url).json()
|
||||
|
||||
ip_info = {
|
||||
"city": resp.get("city")
|
||||
}
|
||||
ip_info = {"city": resp.get("city")}
|
||||
return ip_info
|
||||
except Exception as e:
|
||||
logging.error(f"Error getting client ip info: {e}")
|
||||
@@ -87,7 +93,7 @@ def read_config(config_path):
|
||||
|
||||
def write_json_file(file_path, data):
|
||||
"""将数据写入 JSON 文件"""
|
||||
with open(file_path, 'w', encoding='utf-8') as file:
|
||||
with open(file_path, "w", encoding="utf-8") as file:
|
||||
json.dump(data, file, ensure_ascii=False, indent=4)
|
||||
|
||||
|
||||
@@ -95,21 +101,28 @@ def is_punctuation_or_emoji(char):
|
||||
"""检查字符是否为空格、指定标点或表情符号"""
|
||||
# 定义需要去除的中英文标点(包括全角/半角)
|
||||
punctuation_set = {
|
||||
',', ',', # 中文逗号 + 英文逗号
|
||||
'。', '.', # 中文句号 + 英文句号
|
||||
'!', '!', # 中文感叹号 + 英文感叹号
|
||||
'-', '-', # 英文连字符 + 中文全角横线
|
||||
'、' # 中文顿号
|
||||
",",
|
||||
",", # 中文逗号 + 英文逗号
|
||||
"。",
|
||||
".", # 中文句号 + 英文句号
|
||||
"!",
|
||||
"!", # 中文感叹号 + 英文感叹号
|
||||
"-",
|
||||
"-", # 英文连字符 + 中文全角横线
|
||||
"、", # 中文顿号
|
||||
}
|
||||
if char.isspace() or char in punctuation_set:
|
||||
return True
|
||||
# 检查表情符号(保留原有逻辑)
|
||||
code_point = ord(char)
|
||||
emoji_ranges = [
|
||||
(0x1F600, 0x1F64F), (0x1F300, 0x1F5FF),
|
||||
(0x1F680, 0x1F6FF), (0x1F900, 0x1F9FF),
|
||||
(0x1FA70, 0x1FAFF), (0x2600, 0x26FF),
|
||||
(0x2700, 0x27BF)
|
||||
(0x1F600, 0x1F64F),
|
||||
(0x1F300, 0x1F5FF),
|
||||
(0x1F680, 0x1F6FF),
|
||||
(0x1F900, 0x1F9FF),
|
||||
(0x1FA70, 0x1FAFF),
|
||||
(0x2600, 0x26FF),
|
||||
(0x2700, 0x27BF),
|
||||
]
|
||||
return any(start <= code_point <= end for start, end in emoji_ranges)
|
||||
|
||||
@@ -125,27 +138,42 @@ def get_string_no_punctuation_or_emoji(s):
|
||||
end = len(chars) - 1
|
||||
while end >= start and is_punctuation_or_emoji(chars[end]):
|
||||
end -= 1
|
||||
return ''.join(chars[start:end + 1])
|
||||
return "".join(chars[start : end + 1])
|
||||
|
||||
|
||||
def remove_punctuation_and_length(text):
|
||||
# 全角符号和半角符号的Unicode范围
|
||||
full_width_punctuations = '!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~'
|
||||
full_width_punctuations = (
|
||||
"!"#$%&'()*+,-。/:;<=>?@[\]^_`{|}~"
|
||||
)
|
||||
half_width_punctuations = r'!"#$%&\'()*+,-./:;<=>?@[\]^_`{|}~'
|
||||
space = ' ' # 半角空格
|
||||
full_width_space = ' ' # 全角空格
|
||||
space = " " # 半角空格
|
||||
full_width_space = " " # 全角空格
|
||||
|
||||
# 去除全角和半角符号以及空格
|
||||
result = ''.join([char for char in text if
|
||||
char not in full_width_punctuations and char not in half_width_punctuations and char not in space and char not in full_width_space])
|
||||
result = "".join(
|
||||
[
|
||||
char
|
||||
for char in text
|
||||
if char not in full_width_punctuations
|
||||
and char not in half_width_punctuations
|
||||
and char not in space
|
||||
and char not in full_width_space
|
||||
]
|
||||
)
|
||||
|
||||
if result == "Yeah":
|
||||
return 0, ""
|
||||
return len(result), result
|
||||
|
||||
|
||||
def check_model_key(modelType, modelKey):
|
||||
if "你" in modelKey:
|
||||
logging.error("你还没配置" + modelType + "的密钥,请在配置文件中配置密钥,否则无法正常工作")
|
||||
logging.error(
|
||||
"你还没配置"
|
||||
+ modelType
|
||||
+ "的密钥,请在配置文件中配置密钥,否则无法正常工作"
|
||||
)
|
||||
return False
|
||||
return True
|
||||
|
||||
@@ -155,15 +183,15 @@ def check_ffmpeg_installed():
|
||||
try:
|
||||
# 执行ffmpeg -version命令,并捕获输出
|
||||
result = subprocess.run(
|
||||
['ffmpeg', '-version'],
|
||||
["ffmpeg", "-version"],
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
text=True,
|
||||
check=True # 如果返回码非零则抛出异常
|
||||
check=True, # 如果返回码非零则抛出异常
|
||||
)
|
||||
# 检查输出中是否包含版本信息(可选)
|
||||
output = result.stdout + result.stderr
|
||||
if 'ffmpeg version' in output.lower():
|
||||
if "ffmpeg version" in output.lower():
|
||||
ffmpeg_installed = True
|
||||
return False
|
||||
except (subprocess.CalledProcessError, FileNotFoundError):
|
||||
@@ -175,11 +203,12 @@ def check_ffmpeg_installed():
|
||||
error_msg += "1、按照项目的安装文档,正确进入conda环境\n"
|
||||
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
|
||||
raise ValueError(error_msg)
|
||||
|
||||
|
||||
|
||||
def extract_json_from_string(input_string):
|
||||
"""提取字符串中的 JSON 部分"""
|
||||
pattern = r'(\{.*\})'
|
||||
pattern = r"(\{.*\})"
|
||||
match = re.search(pattern, input_string)
|
||||
if match:
|
||||
return match.group(1) # 返回提取的 JSON 字符串
|
||||
return None
|
||||
return None
|
||||
|
||||
@@ -31,7 +31,7 @@ class SileroVAD(VAD):
|
||||
def is_vad(self, conn, opus_packet):
|
||||
try:
|
||||
pcm_frame = self.decoder.decode(opus_packet, 960)
|
||||
conn.client_audio_buffer += pcm_frame # 将新数据加入缓冲区
|
||||
conn.client_audio_buffer.extend(pcm_frame) # 将新数据加入缓冲区
|
||||
|
||||
# 处理缓冲区中的完整帧(每次处理512采样点)
|
||||
client_have_voice = False
|
||||
|
||||
Reference in New Issue
Block a user