mirror of
https://github.com/xinnan-tech/xiaozhi-esp32-server.git
synced 2026-07-23 23:53:55 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2b662d3a14 | ||
|
|
4f3fae81c1 | ||
|
|
7b266ad7c0 | ||
|
|
16601a67e3 | ||
|
|
224d59d50d |
+7
-7
@@ -93,7 +93,7 @@ docker logs -f xiaozhi-esp32-server
|
|||||||
```
|
```
|
||||||
docker stop xiaozhi-esp32-server
|
docker stop xiaozhi-esp32-server
|
||||||
docker rm xiaozhi-esp32-server
|
docker rm xiaozhi-esp32-server
|
||||||
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
docker rmi ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:server_latest
|
||||||
```
|
```
|
||||||
|
|
||||||
3、重新按docker方式部署
|
3、重新按docker方式部署
|
||||||
@@ -299,14 +299,14 @@ LLM:
|
|||||||
|
|
||||||
以下是一些常见问题,供参考:
|
以下是一些常见问题,供参考:
|
||||||
|
|
||||||
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
[1、为什么我说的话,小智识别出来很多韩文、日文、英文](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
|
|
||||||
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
[2、为什么会出现“TTS 任务出错 文件不存在”?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
|
|
||||||
[3、TTS 经常失败,经常超时](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
[3、TTS 经常失败,经常超时](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
|
|
||||||
[4、如何提高小智对话响应速度?](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
[4、如何提高小智对话响应速度?](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
|
|
||||||
[5、我说话很慢,停顿时小智老是抢话](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
[5、我说话很慢,停顿时小智老是抢话](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
|
|
||||||
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1tts-%E7%BB%8F%E5%B8%B8%E5%A4%B1%E8%B4%A5%E7%BB%8F%E5%B8%B8%E8%B6%85%E6%97%B6-)
|
[6、我想通过小智控制电灯、空调、远程开关机等操作](../README.md#1%E4%B8%BA%E4%BB%80%E4%B9%88%E6%88%91%E8%AF%B4%E7%9A%84%E8%AF%9D%E5%B0%8F%E6%99%BA%E8%AF%86%E5%88%AB%E5%87%BA%E6%9D%A5%E5%BE%88%E5%A4%9A%E9%9F%A9%E6%96%87%E6%97%A5%E6%96%87%E8%8B%B1%E6%96%87)
|
||||||
|
|||||||
@@ -43,4 +43,19 @@ export default {
|
|||||||
})
|
})
|
||||||
}).send()
|
}).send()
|
||||||
},
|
},
|
||||||
|
// 解绑设备
|
||||||
|
unbindDevice(device_id, callback) {
|
||||||
|
RequestService.sendRequest()
|
||||||
|
.url(`${getServiceUrl()}/api/v1/user/device/unbind/${device_id}`)
|
||||||
|
.method('PUT')
|
||||||
|
.success((res) => {
|
||||||
|
RequestService.clearRequestTime();
|
||||||
|
callback(res);
|
||||||
|
})
|
||||||
|
.fail(() => {
|
||||||
|
RequestService.reAjaxFun(() => {
|
||||||
|
this.unbindDevice(device_id, callback);
|
||||||
|
});
|
||||||
|
}).send()
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,9 @@
|
|||||||
const HAVE_NO_RESULT = '暂无'
|
const HAVE_NO_RESULT = '暂无'
|
||||||
export default {
|
export default {
|
||||||
HAVE_NO_RESULT, // 项目的配置信息
|
HAVE_NO_RESULT, // 项目的配置信息
|
||||||
|
PAGE: {
|
||||||
|
LOGIN: '/login',
|
||||||
|
},
|
||||||
STORAGE_KEY: {
|
STORAGE_KEY: {
|
||||||
TOKEN: 'TOKEN',
|
TOKEN: 'TOKEN',
|
||||||
PUBLIC_KEY: 'PUBLIC_KEY',
|
PUBLIC_KEY: 'PUBLIC_KEY',
|
||||||
|
|||||||
@@ -21,9 +21,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px">
|
<div style="display: flex;align-items: center;gap: 8px;margin-top: 10px">
|
||||||
<div class="serach-box">
|
<div class="serach-box">
|
||||||
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" />
|
<el-input placeholder="输入名称搜索.." v-model="serach" style="border: none; background: transparent;" @keyup.enter.native="handleSearch" />
|
||||||
<img src="@/assets/home/search.png" alt=""
|
<img src="@/assets/home/search.png" alt=""
|
||||||
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" />
|
style="width: 12px;height: 12px;margin-right: 11px;cursor: pointer;" @click="handleSearch" />
|
||||||
</div>
|
</div>
|
||||||
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
<img src="@/assets/home/avatar.png" alt="" style="width: 21px;height: 21px;" />
|
||||||
<div class="user-info">
|
<div class="user-info">
|
||||||
@@ -56,16 +56,16 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: space-between;box-sizing: border-box;">
|
style="display: flex;flex-wrap: wrap;margin-top: 15px;gap: 15px;justify-content: flex-start;box-sizing: border-box;">
|
||||||
<div class="device-item" v-for="(item,index) in deviceList" :key="index">
|
<div class="device-item" v-for="(item,index) in filteredDeviceList" :key="index">
|
||||||
<div style="display: flex;justify-content: space-between;">
|
<div style="display: flex;justify-content: space-between; align-items: center; ">
|
||||||
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
|
<div style="font-weight: 700;font-size: 18px;text-align: left;color: #3d4566;">
|
||||||
<!-- CC:ba:97:11:a6:ac-->
|
<!-- CC:ba:97:11:a6:ac-->
|
||||||
{{item.list[0]?.mac_address}}
|
{{item.list[0]?.mac_address}}
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div style="display: flex;align-items: center;">
|
||||||
<img src="@/assets/home/delete.png" alt=""
|
<img src="@/assets/home/delete.png" alt=""
|
||||||
style="width: 18px;height: 18px;margin-right: 8px;" />
|
style="width: 18px;height: 18px;margin-right: 8px;" @click="unbindDevice(item.list[0]?.id)" />
|
||||||
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
<img src="@/assets/home/info.png" alt="" style="width: 18px;height: 18px;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +229,9 @@ export default {
|
|||||||
name: 'home',
|
name: 'home',
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
serach: '',
|
serach: '', // 搜索框输入内容
|
||||||
|
deviceList: [], // 原始设备列表
|
||||||
|
filteredDeviceList: [], // 过滤后的设备列表
|
||||||
switchValue: false,
|
switchValue: false,
|
||||||
addDeviceDialogVisible: false,
|
addDeviceDialogVisible: false,
|
||||||
deviceCode: "",
|
deviceCode: "",
|
||||||
@@ -250,8 +252,7 @@ export default {
|
|||||||
}],
|
}],
|
||||||
userInfo: {
|
userInfo: {
|
||||||
mobile: '' // 初始化用户信息
|
mobile: '' // 初始化用户信息
|
||||||
},
|
}
|
||||||
deviceList:[]
|
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
@@ -270,14 +271,51 @@ export default {
|
|||||||
// 获取已绑设备
|
// 获取已绑设备
|
||||||
getList(){
|
getList(){
|
||||||
Api.user.getHomeList(({data})=>{
|
Api.user.getHomeList(({data})=>{
|
||||||
console.log(data.data)
|
this.deviceList = data.data; // 保存原始设备列表
|
||||||
this.deviceList = data.data
|
this.filteredDeviceList = data.data; // 初始化过滤后的设备列表
|
||||||
})
|
})
|
||||||
}
|
},
|
||||||
|
// 处理搜索
|
||||||
|
handleSearch() {
|
||||||
|
if (this.serach.trim() === '') {
|
||||||
|
// 如果搜索框为空,显示全部设备
|
||||||
|
this.filteredDeviceList = this.deviceList;
|
||||||
|
} else {
|
||||||
|
// 过滤设备列表
|
||||||
|
this.filteredDeviceList = this.deviceList.filter(device => {
|
||||||
|
return (
|
||||||
|
device.list[0]?.mac_address?.includes(this.serach) || // 匹配MAC地址
|
||||||
|
device.list[0]?.device_type?.includes(this.serach) || // 匹配设备型号
|
||||||
|
device.list[0]?.app_version?.includes(this.serach) // 匹配APP版本
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 解绑设备
|
||||||
|
unbindDevice(device_id) {
|
||||||
|
this.$confirm('确定要解绑该设备吗?', '提示', {
|
||||||
|
confirmButtonText: '确定',
|
||||||
|
cancelButtonText: '取消',
|
||||||
|
type: 'warning'
|
||||||
|
}).then(() => {
|
||||||
|
// 调用解绑设备的接口
|
||||||
|
Api.user.unbindDevice(device_id, ({ data }) => {
|
||||||
|
if (data.code === 0) {
|
||||||
|
this.$message.success('解绑成功');
|
||||||
|
this.getList();
|
||||||
|
} else {
|
||||||
|
this.$message.error(data.msg || '解绑失败');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}).catch(() => {
|
||||||
|
// 用户取消操作
|
||||||
|
this.$message.info('已取消解绑');
|
||||||
|
});
|
||||||
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.fetchUserInfo(); // 组件加载时获取用户信息
|
this.fetchUserInfo(); // 组件加载时获取用户信息
|
||||||
this.getList()
|
this.getList(); // 初始化设备列表
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import websockets
|
|||||||
from typing import Dict, Any
|
from typing import Dict, Any
|
||||||
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
|
from core.utils.util import get_string_no_punctuation_or_emoji, extract_json_from_string
|
||||||
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
|
||||||
@@ -307,7 +307,6 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
response_message = []
|
response_message = []
|
||||||
processed_chars = 0 # 跟踪已处理的字符位置
|
processed_chars = 0 # 跟踪已处理的字符位置
|
||||||
function_call_data = None # 存储function call数据
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
@@ -316,7 +315,7 @@ class ConnectionHandler:
|
|||||||
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"记忆内容: {memory_str}")
|
#self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
|
||||||
|
|
||||||
# 使用支持functions的streaming接口
|
# 使用支持functions的streaming接口
|
||||||
llm_responses = self.llm.response_with_functions(
|
llm_responses = self.llm.response_with_functions(
|
||||||
@@ -332,48 +331,61 @@ class ConnectionHandler:
|
|||||||
text_index = 0
|
text_index = 0
|
||||||
|
|
||||||
# 处理流式响应
|
# 处理流式响应
|
||||||
|
tool_call_flag = False
|
||||||
|
function_name = None
|
||||||
|
function_id = None
|
||||||
|
function_arguments = ""
|
||||||
|
content_arguments = ""
|
||||||
for response in llm_responses:
|
for response in llm_responses:
|
||||||
if response["type"] == "content":
|
content, tools_call = response
|
||||||
content = response["content"]
|
if content is not None and len(content)>0:
|
||||||
response_message.append(content)
|
if len(response_message)<=0 and content=="```":
|
||||||
|
tool_call_flag = True
|
||||||
|
|
||||||
if self.client_abort:
|
if tools_call is not None:
|
||||||
break
|
tool_call_flag = True
|
||||||
|
if tools_call[0].id is not None:
|
||||||
|
function_id = tools_call[0].id
|
||||||
|
if tools_call[0].function.name is not None:
|
||||||
|
function_name = tools_call[0].function.name
|
||||||
|
if tools_call[0].function.arguments is not None:
|
||||||
|
function_arguments += tools_call[0].function.arguments
|
||||||
|
|
||||||
end_time = time.time()
|
if content is not None and len(content) > 0:
|
||||||
self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
|
if tool_call_flag:
|
||||||
|
content_arguments+=content
|
||||||
|
else:
|
||||||
|
response_message.append(content)
|
||||||
|
|
||||||
# 处理文本分段和TTS逻辑
|
if self.client_abort:
|
||||||
# 合并当前全部文本并处理未分割部分
|
break
|
||||||
full_text = "".join(response_message)
|
|
||||||
current_text = full_text[processed_chars:] # 从未处理的位置开始
|
|
||||||
|
|
||||||
# 查找最后一个有效标点
|
end_time = time.time()
|
||||||
punctuations = ("。", "?", "!", ";", ":")
|
self.logger.bind(tag=TAG).debug(f"大模型返回时间: {end_time - start_time} 秒, 生成token={content}")
|
||||||
last_punct_pos = -1
|
|
||||||
for punct in punctuations:
|
|
||||||
pos = current_text.rfind(punct)
|
|
||||||
if pos > last_punct_pos:
|
|
||||||
last_punct_pos = pos
|
|
||||||
|
|
||||||
# 找到分割点则处理
|
# 处理文本分段和TTS逻辑
|
||||||
if last_punct_pos != -1:
|
# 合并当前全部文本并处理未分割部分
|
||||||
segment_text_raw = current_text[:last_punct_pos + 1]
|
full_text = "".join(response_message)
|
||||||
segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
|
current_text = full_text[processed_chars:] # 从未处理的位置开始
|
||||||
if segment_text:
|
|
||||||
text_index += 1
|
|
||||||
self.recode_first_last_text(segment_text, text_index)
|
|
||||||
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
|
|
||||||
self.tts_queue.put(future)
|
|
||||||
processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
|
||||||
|
|
||||||
elif response["type"] == "function_call":
|
# 查找最后一个有效标点
|
||||||
# Extract function call data
|
punctuations = ("。", "?", "!", ";", ":")
|
||||||
function_call_data = {
|
last_punct_pos = -1
|
||||||
"name": response["function_call"]["function"]["name"],
|
for punct in punctuations:
|
||||||
"arguments": response["function_call"]["function"]["arguments"]
|
pos = current_text.rfind(punct)
|
||||||
}
|
if pos > last_punct_pos:
|
||||||
self.logger.bind(tag=TAG).info(f"Function call detected: {function_call_data}")
|
last_punct_pos = pos
|
||||||
|
|
||||||
|
# 找到分割点则处理
|
||||||
|
if last_punct_pos != -1:
|
||||||
|
segment_text_raw = current_text[:last_punct_pos + 1]
|
||||||
|
segment_text = get_string_no_punctuation_or_emoji(segment_text_raw)
|
||||||
|
if segment_text:
|
||||||
|
text_index += 1
|
||||||
|
self.recode_first_last_text(segment_text, text_index)
|
||||||
|
future = self.executor.submit(self.speak_and_play, segment_text, text_index)
|
||||||
|
self.tts_queue.put(future)
|
||||||
|
processed_chars += len(segment_text_raw) # 更新已处理字符位置
|
||||||
|
|
||||||
# 处理最后剩余的文本
|
# 处理最后剩余的文本
|
||||||
full_text = "".join(response_message)
|
full_text = "".join(response_message)
|
||||||
@@ -387,23 +399,46 @@ class ConnectionHandler:
|
|||||||
self.tts_queue.put(future)
|
self.tts_queue.put(future)
|
||||||
|
|
||||||
# 存储对话内容
|
# 存储对话内容
|
||||||
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
if len(response_message)>0:
|
||||||
|
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
|
||||||
|
|
||||||
# 处理function call
|
# 处理function call
|
||||||
if function_call_data:
|
if tool_call_flag:
|
||||||
|
if function_id is None:
|
||||||
|
a = extract_json_from_string(content_arguments)
|
||||||
|
if a is not None:
|
||||||
|
content_arguments_json = json.loads(a)
|
||||||
|
function_name = content_arguments_json["function_name"]
|
||||||
|
function_arguments = json.dumps(content_arguments_json["args"], ensure_ascii=False)
|
||||||
|
function_id = str(uuid.uuid4().hex)
|
||||||
|
else:
|
||||||
|
return []
|
||||||
|
function_arguments = json.loads(function_arguments)
|
||||||
|
self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={function_arguments}")
|
||||||
|
function_call_data = {
|
||||||
|
"name": function_name,
|
||||||
|
"id": function_id,
|
||||||
|
"arguments": function_arguments
|
||||||
|
}
|
||||||
result = handle_llm_function_call(self, function_call_data)
|
result = handle_llm_function_call(self, function_call_data)
|
||||||
if result.action == Action.RESPONSE:
|
self._handle_function_result(result, function_call_data, text_index+1)
|
||||||
text = result.response
|
|
||||||
text_index += 1
|
|
||||||
self.recode_first_last_text(text, text_index)
|
|
||||||
future = self.executor.submit(self.speak_and_play, text, text_index)
|
|
||||||
self.tts_queue.put(future)
|
|
||||||
|
|
||||||
self.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
|
||||||
|
|
||||||
|
def _handle_function_result(self, result, function_call_data, text_index):
|
||||||
|
if result.action == Action.RESPONSE: # 直接回复前端
|
||||||
|
text = result.response
|
||||||
|
self.recode_first_last_text(text, text_index)
|
||||||
|
future = self.executor.submit(self.speak_and_play, text, text_index)
|
||||||
|
self.tts_queue.put(future)
|
||||||
|
self.dialogue.put(Message(role="assistant", content=text))
|
||||||
|
if result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
|
||||||
|
text = result.response
|
||||||
|
|
||||||
|
|
||||||
def _tts_priority_thread(self):
|
def _tts_priority_thread(self):
|
||||||
while not self.stop_event.is_set():
|
while not self.stop_event.is_set():
|
||||||
text = None
|
text = None
|
||||||
|
|||||||
@@ -122,12 +122,10 @@ async def analyze_intent_with_llm(conn, text):
|
|||||||
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
logger.bind(tag=TAG).warning("意图识别服务未初始化")
|
||||||
return None
|
return None
|
||||||
|
|
||||||
# 创建对话历史记录
|
# 对话历史记录
|
||||||
dialogue = conn.dialogue
|
dialogue = conn.dialogue
|
||||||
dialogue.put(Message(role="user", content=text))
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
intent_result = await conn.intent.detect_intent(dialogue.dialogue)
|
intent_result = await conn.intent.detect_intent(dialogue.dialogue, text)
|
||||||
logger.bind(tag=TAG).info(f"意图识别结果: {intent_result}")
|
logger.bind(tag=TAG).info(f"意图识别结果: {intent_result}")
|
||||||
|
|
||||||
# 尝试解析JSON结果
|
# 尝试解析JSON结果
|
||||||
|
|||||||
@@ -67,8 +67,9 @@ async def no_voice_close_connect(conn):
|
|||||||
else:
|
else:
|
||||||
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
no_voice_time = time.time() * 1000 - conn.client_no_voice_last_time
|
||||||
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
|
close_connection_no_voice_time = conn.config.get("close_connection_no_voice_time", 120)
|
||||||
if no_voice_time > 1000 * close_connection_no_voice_time:
|
if not conn.close_after_chat and no_voice_time > 1000 * close_connection_no_voice_time:
|
||||||
|
conn.close_after_chat = True
|
||||||
conn.client_abort = False
|
conn.client_abort = False
|
||||||
conn.asr_server_receive = False
|
conn.asr_server_receive = False
|
||||||
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜”为结尾"
|
prompt = "请你以“时间过得真快”未来头,用富有感情、依依不舍的话来结束这场对话吧。"
|
||||||
await startToChat(conn, prompt)
|
await startToChat(conn, prompt)
|
||||||
|
|||||||
@@ -13,28 +13,37 @@ async def sendAudioMessage(conn, audios, text, text_index=0):
|
|||||||
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
logger.bind(tag=TAG).info(f"发送第一段语音: {text}")
|
||||||
await send_tts_message(conn, "sentence_start", text)
|
await send_tts_message(conn, "sentence_start", text)
|
||||||
|
|
||||||
# 初始化流控参数
|
# 流控参数优化
|
||||||
frame_duration = 60 # 毫秒
|
original_frame_duration = 60 # 原始帧时长(毫秒)
|
||||||
start_time = time.perf_counter() # 使用高精度计时器
|
adjusted_frame_duration = int(original_frame_duration * 0.8) # 缩短20%
|
||||||
play_position = 0 # 已播放的时长(毫秒)
|
total_frames = len(audios) # 获取总帧数
|
||||||
|
compensation = total_frames * (original_frame_duration - adjusted_frame_duration) / 1000 # 补偿时间(秒)
|
||||||
|
|
||||||
|
start_time = time.perf_counter()
|
||||||
|
play_position = 0 # 已播放时长(毫秒)
|
||||||
|
|
||||||
for opus_packet in audios:
|
for opus_packet in audios:
|
||||||
if conn.client_abort:
|
if conn.client_abort:
|
||||||
return
|
return
|
||||||
|
|
||||||
# 计算当前包的预期发送时间
|
# 计算带加速因子的预期时间
|
||||||
expected_time = start_time + (play_position / 1000)
|
expected_time = start_time + (play_position / 1000)
|
||||||
current_time = time.perf_counter()
|
current_time = time.perf_counter()
|
||||||
|
|
||||||
# 等待直到预期时间
|
# 流控等待(使用加速后的帧时长)
|
||||||
delay = expected_time - current_time
|
delay = expected_time - current_time
|
||||||
if delay > 0:
|
if delay > 0:
|
||||||
await asyncio.sleep(delay)
|
await asyncio.sleep(delay)
|
||||||
|
|
||||||
# 发送音频包
|
|
||||||
await conn.websocket.send(opus_packet)
|
await conn.websocket.send(opus_packet)
|
||||||
play_position += frame_duration # 更新播放位置
|
play_position += adjusted_frame_duration # 使用调整后的帧时长
|
||||||
|
|
||||||
|
# 补偿因加速损失的时长
|
||||||
|
if compensation > 0:
|
||||||
|
await asyncio.sleep(compensation)
|
||||||
|
|
||||||
await send_tts_message(conn, "sentence_end", text)
|
await send_tts_message(conn, "sentence_end", text)
|
||||||
|
|
||||||
# 发送结束消息(如果是最后一个文本)
|
# 发送结束消息(如果是最后一个文本)
|
||||||
if conn.llm_finish_task and text_index == conn.tts_last_text_index:
|
if conn.llm_finish_task and text_index == conn.tts_last_text_index:
|
||||||
await send_tts_message(conn, 'stop', None)
|
await send_tts_message(conn, 'stop', None)
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ from config.logger import setup_logging
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class IntentProviderBase(ABC):
|
class IntentProviderBase(ABC):
|
||||||
def __init__(self, config):
|
def __init__(self, config):
|
||||||
self.config = config
|
self.config = config
|
||||||
@@ -19,7 +20,7 @@ class IntentProviderBase(ABC):
|
|||||||
logger.bind(tag=TAG).debug("Set LLM for intent provider")
|
logger.bind(tag=TAG).debug("Set LLM for intent provider")
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
async def detect_intent(self, dialogue_history: List[Dict]) -> str:
|
async def detect_intent(self, dialogue_history: List[Dict], text: str) -> str:
|
||||||
"""
|
"""
|
||||||
检测用户最后一句话的意图
|
检测用户最后一句话的意图
|
||||||
Args:
|
Args:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@ class IntentProvider(IntentProviderBase):
|
|||||||
)
|
)
|
||||||
return prompt
|
return prompt
|
||||||
|
|
||||||
async def detect_intent(self, dialogue_history: List[Dict]) -> str:
|
async def detect_intent(self, dialogue_history: List[Dict], text:str) -> str:
|
||||||
if not self.llm:
|
if not self.llm:
|
||||||
raise ValueError("LLM provider not set")
|
raise ValueError("LLM provider not set")
|
||||||
|
|
||||||
@@ -48,9 +48,8 @@ class IntentProvider(IntentProviderBase):
|
|||||||
msgStr += f"User: {msg.content}\n"
|
msgStr += f"User: {msg.content}\n"
|
||||||
elif msg.role== "assistant":
|
elif msg.role== "assistant":
|
||||||
msgStr += f"Assistant: {msg.content}\n"
|
msgStr += f"Assistant: {msg.content}\n"
|
||||||
|
msgStr += f"User: {text}\n"
|
||||||
user_prompt = f"请分析用户的意图:\n{msgStr}"
|
user_prompt = f"请分析用户的意图:\n{msgStr}"
|
||||||
|
|
||||||
# 使用LLM进行意图识别
|
# 使用LLM进行意图识别
|
||||||
intent = self.llm.response_no_stream(
|
intent = self.llm.response_no_stream(
|
||||||
system_prompt=self.promot,
|
system_prompt=self.promot,
|
||||||
|
|||||||
@@ -5,12 +5,14 @@ from config.logger import setup_logging
|
|||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
class IntentProvider(IntentProviderBase):
|
class IntentProvider(IntentProviderBase):
|
||||||
async def detect_intent(self, dialogue_history: List[Dict]) -> str:
|
async def detect_intent(self, dialogue_history: List[Dict], text: str) -> str:
|
||||||
"""
|
"""
|
||||||
默认的意图识别实现,始终返回继续聊天
|
默认的意图识别实现,始终返回继续聊天
|
||||||
Args:
|
Args:
|
||||||
dialogue_history: 对话历史记录列表
|
dialogue_history: 对话历史记录列表
|
||||||
|
text: 本次对话记录
|
||||||
Returns:
|
Returns:
|
||||||
固定返回"继续聊天"
|
固定返回"继续聊天"
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -51,30 +51,8 @@ class LLMProvider(LLMProviderBase):
|
|||||||
tools=functions,
|
tools=functions,
|
||||||
)
|
)
|
||||||
|
|
||||||
current_function_call = None
|
|
||||||
current_content = ""
|
|
||||||
|
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
delta = chunk.choices[0].delta
|
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||||
|
|
||||||
if delta.content:
|
|
||||||
current_content += delta.content
|
|
||||||
yield {"type": "content", "content": delta.content}
|
|
||||||
|
|
||||||
if delta.tool_calls:
|
|
||||||
tool_call = delta.tool_calls[0]
|
|
||||||
# Handle the function call data using proper attribute access
|
|
||||||
if not current_function_call:
|
|
||||||
current_function_call = {
|
|
||||||
"function": {
|
|
||||||
"name": tool_call.function.name,
|
|
||||||
"arguments": tool_call.function.arguments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_function_call:
|
|
||||||
logger.bind(tag=TAG).debug(f"ollama Function call detected: {current_function_call}")
|
|
||||||
yield {"type": "function_call", "function_call": current_function_call}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
logger.bind(tag=TAG).error(f"Error in Ollama function call: {e}")
|
||||||
|
|||||||
@@ -53,30 +53,8 @@ class LLMProvider(LLMProviderBase):
|
|||||||
tools=functions,
|
tools=functions,
|
||||||
)
|
)
|
||||||
|
|
||||||
current_function_call = None
|
|
||||||
current_content = ""
|
|
||||||
|
|
||||||
for chunk in stream:
|
for chunk in stream:
|
||||||
delta = chunk.choices[0].delta
|
yield chunk.choices[0].delta.content, chunk.choices[0].delta.tool_calls
|
||||||
|
|
||||||
if delta.content:
|
|
||||||
current_content += delta.content
|
|
||||||
yield {"type": "content", "content": delta.content}
|
|
||||||
|
|
||||||
if delta.tool_calls:
|
|
||||||
tool_call = delta.tool_calls[0]
|
|
||||||
# Handle the function call data using proper attribute access
|
|
||||||
if not current_function_call:
|
|
||||||
current_function_call = {
|
|
||||||
"function": {
|
|
||||||
"name": tool_call.function.name,
|
|
||||||
"arguments": tool_call.function.arguments
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if current_function_call:
|
|
||||||
logger.bind(tag=TAG).debug(f"openai Function call detected: {current_function_call}")
|
|
||||||
yield {"type": "function_call", "function_call": current_function_call}
|
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
self.logger.bind(tag=TAG).error(f"Error in function call streaming: {e}")
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import yaml
|
|||||||
import socket
|
import socket
|
||||||
import subprocess
|
import subprocess
|
||||||
import logging
|
import logging
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
def get_project_dir():
|
def get_project_dir():
|
||||||
@@ -119,3 +120,11 @@ def check_ffmpeg_installed():
|
|||||||
error_msg += "1、按照项目的安装文档,正确进入conda环境\n"
|
error_msg += "1、按照项目的安装文档,正确进入conda环境\n"
|
||||||
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
|
error_msg += "2、查阅安装文档,如何在conda环境中安装ffmpeg\n"
|
||||||
raise ValueError(error_msg)
|
raise ValueError(error_msg)
|
||||||
|
|
||||||
|
def extract_json_from_string(input_string):
|
||||||
|
"""提取字符串中的 JSON 部分"""
|
||||||
|
pattern = r'(\{.*\})'
|
||||||
|
match = re.search(pattern, input_string)
|
||||||
|
if match:
|
||||||
|
return match.group(1) # 返回提取的 JSON 字符串
|
||||||
|
return None
|
||||||
Reference in New Issue
Block a user