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 | |
|---|---|---|---|
|
|
87cb0b484d | ||
|
|
73803f49fc | ||
|
|
5fd87752d5 | ||
|
|
94a90c4124 | ||
|
|
c3b0347155 | ||
|
|
600f712750 | ||
|
|
9e76869c36 | ||
|
|
cb540736ab | ||
|
|
66cd5bb4a5 | ||
|
|
1226e8be47 | ||
|
|
22ff7a92b8 |
@@ -58,28 +58,13 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 系统要求与部署前提 🖥️
|
## 系统要求与部署前提 🖥️
|
||||||
|
|
||||||
- **硬件**:一套兼容 `xiaozhi-esp32`
|
- **硬件**:一套兼容 `xiaozhi-esp32`
|
||||||
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
的硬件设备(具体型号请参考 [此处](https://rcnv1t9vps13.feishu.cn/wiki/DdgIw4BUgivWDPkhMj1cGIYCnRf))。
|
||||||
- **服务器**:至少 4 核 CPU、8G 内存的电脑或服务器。
|
- **服务器**:至少 4 核 CPU、8G 内存的电脑。
|
||||||
- **固件编译**:请将后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译固件并烧录到设备上。
|
- **固件编译**:请将后端服务的接口地址更新至 `xiaozhi-esp32` 项目中,再重新编译固件并烧录到设备上。
|
||||||
|
|
||||||
---
|
---
|
||||||
@@ -178,17 +163,25 @@ server:
|
|||||||
|
|
||||||
### 一、[部署文档](./docs/Deployment.md)
|
### 一、[部署文档](./docs/Deployment.md)
|
||||||
|
|
||||||
本项目支持以下三种部署方式,您可根据实际需求选择:
|
本项目支持以下三种部署方式,您可根据实际需求选择。
|
||||||
|
|
||||||
|
本项目的文档主要是`文字版本`的教程,如果你想要`视频版本`的教程,您可以学习一下[这个大佬的手把手教程](https://www.bilibili.com/video/BV1gePuejEvT)。
|
||||||
|
|
||||||
|
如果你能把`文字版本的教程`和`视频版本的教程`结合起来一起看,可以让你更快上手。
|
||||||
|
|
||||||
1. [Docker 快速部署](./docs/Deployment.md)
|
1. [Docker 快速部署](./docs/Deployment.md)
|
||||||
适合快速体验,不需过多环境配置。缺点是,拉取镜像有点慢。
|
|
||||||
|
适合快速体验的普通用户,不需过多环境配置。缺点是,拉取镜像有点慢。
|
||||||
|
|
||||||
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
|
2. [借助 Docker 环境运行部署](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%BA%8C%E5%80%9F%E5%8A%A9docker%E7%8E%AF%E5%A2%83%E8%BF%90%E8%A1%8C%E9%83%A8%E7%BD%B2)
|
||||||
适用于已安装 Docker 且希望对代码进行自定义修改的用户。
|
|
||||||
|
适用于已安装 Docker 且希望对代码进行自定义修改的软件工程师。
|
||||||
|
|
||||||
3. [本地源码运行](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
|
3. [本地源码运行](./docs/Deployment.md#%E6%96%B9%E5%BC%8F%E4%B8%89%E6%9C%AC%E5%9C%B0%E6%BA%90%E7%A0%81%E8%BF%90%E8%A1%8C)
|
||||||
适合熟悉 Conda 环境或希望从零搭建运行环境的用户。
|
|
||||||
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。
|
适合熟悉`Conda` 环境或希望从零搭建运行环境的用户。
|
||||||
|
|
||||||
|
对于对响应速度要求较高的场景,推荐使用本地源码运行方式以降低额外开销。
|
||||||
|
|
||||||
### 二、[固件编译](./docs/firmware-build.md)
|
### 二、[固件编译](./docs/firmware-build.md)
|
||||||
|
|
||||||
@@ -304,7 +297,6 @@ TTS 性能排行:
|
|||||||
## 鸣谢 🙏
|
## 鸣谢 🙏
|
||||||
|
|
||||||
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
|
- 本项目受 [百聆语音对话机器人](https://github.com/wwbin2017/bailing) 启发,并在其基础上实现。
|
||||||
- 感谢 [腾讯云](https://cloud.tencent.com/) 提供免费 Docker 镜像空间。
|
|
||||||
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
|
- 感谢 [十方融海](https://www.tenclass.com/) 对小智通讯协议提供的详尽文档支持。
|
||||||
|
|
||||||
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
<a href="https://star-history.com/#xinnan-tech/xiaozhi-esp32-server&Date">
|
||||||
|
|||||||
+25
-1
@@ -41,7 +41,9 @@ manager:
|
|||||||
enabled: false
|
enabled: false
|
||||||
ip: 0.0.0.0
|
ip: 0.0.0.0
|
||||||
port: 8002
|
port: 8002
|
||||||
|
iot:
|
||||||
|
Speaker:
|
||||||
|
volume: 100
|
||||||
xiaozhi:
|
xiaozhi:
|
||||||
type: hello
|
type: hello
|
||||||
version: 1
|
version: 1
|
||||||
@@ -147,6 +149,12 @@ LLM:
|
|||||||
user_id: 你的user_id
|
user_id: 你的user_id
|
||||||
base_url: "https://api.coze.cn/open_api/v2/chat" # 服务地址
|
base_url: "https://api.coze.cn/open_api/v2/chat" # 服务地址
|
||||||
personal_access_token: 你的coze个人令牌
|
personal_access_token: 你的coze个人令牌
|
||||||
|
LMStudioLLM:
|
||||||
|
# 定义LLM API类型
|
||||||
|
type: openai
|
||||||
|
model_name: deepseek-r1-distill-llama-8b@q4_k_m # 使用的模型名称,需要预先在社区下载
|
||||||
|
url: http://localhost:1234/v1 # LM Studio服务地址
|
||||||
|
api_key: lm-studio # LM Studio服务的固定API Key
|
||||||
HomeAssistant:
|
HomeAssistant:
|
||||||
# 定义LLM API类型
|
# 定义LLM API类型
|
||||||
type: homeassistant
|
type: homeassistant
|
||||||
@@ -304,3 +312,19 @@ module_test:
|
|||||||
- "你好,请介绍一下你自己"
|
- "你好,请介绍一下你自己"
|
||||||
- "What's the weather like today?"
|
- "What's the weather like today?"
|
||||||
- "请用100字概括量子计算的基本原理和应用前景"
|
- "请用100字概括量子计算的基本原理和应用前景"
|
||||||
|
|
||||||
|
# 本地音乐播放配置
|
||||||
|
music:
|
||||||
|
music_commands:
|
||||||
|
- "来一首歌"
|
||||||
|
- "唱一首歌"
|
||||||
|
- "播放音乐"
|
||||||
|
- "来点音乐"
|
||||||
|
- "背景音乐"
|
||||||
|
- "放首歌"
|
||||||
|
- "播放歌曲"
|
||||||
|
- "来点背景音乐"
|
||||||
|
- "我想听歌"
|
||||||
|
- "我要听歌"
|
||||||
|
- "放点音乐"
|
||||||
|
music_dir: "./music" # 音乐文件存放路径
|
||||||
+26
-14
@@ -4,6 +4,7 @@ import uuid
|
|||||||
import time
|
import time
|
||||||
import queue
|
import queue
|
||||||
import asyncio
|
import asyncio
|
||||||
|
import traceback
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
import threading
|
import threading
|
||||||
import websockets
|
import websockets
|
||||||
@@ -14,15 +15,18 @@ 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
|
||||||
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
from concurrent.futures import ThreadPoolExecutor, TimeoutError
|
||||||
from core.handle.audioHandle import handleAudioMessage, sendAudioMessage
|
from core.handle.sendAudioHandle import sendAudioMessage
|
||||||
|
from core.handle.receiveAudioHandle import handleAudioMessage
|
||||||
from config.private_config import PrivateConfig
|
from config.private_config import PrivateConfig
|
||||||
from core.auth import AuthMiddleware, AuthenticationError
|
from core.auth import AuthMiddleware, AuthenticationError
|
||||||
from core.utils.auth_code_gen import AuthCodeGenerator # 添加导入
|
from core.utils.auth_code_gen import AuthCodeGenerator
|
||||||
|
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
|
|
||||||
|
|
||||||
class ConnectionHandler:
|
class ConnectionHandler:
|
||||||
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts):
|
def __init__(self, config: Dict[str, Any], _vad, _asr, _llm, _tts, _music):
|
||||||
self.config = config
|
self.config = config
|
||||||
self.logger = setup_logging()
|
self.logger = setup_logging()
|
||||||
self.auth = AuthMiddleware(config)
|
self.auth = AuthMiddleware(config)
|
||||||
@@ -72,28 +76,33 @@ class ConnectionHandler:
|
|||||||
self.tts_start_speak_time = None
|
self.tts_start_speak_time = None
|
||||||
self.tts_duration = 0
|
self.tts_duration = 0
|
||||||
|
|
||||||
|
# iot相关变量
|
||||||
|
self.iot_descriptors = {}
|
||||||
|
|
||||||
self.cmd_exit = self.config["CMD_exit"]
|
self.cmd_exit = self.config["CMD_exit"]
|
||||||
self.max_cmd_length = 0
|
self.max_cmd_length = 0
|
||||||
for cmd in self.cmd_exit:
|
for cmd in self.cmd_exit:
|
||||||
if len(cmd) > self.max_cmd_length:
|
if len(cmd) > self.max_cmd_length:
|
||||||
self.max_cmd_length = len(cmd)
|
self.max_cmd_length = len(cmd)
|
||||||
|
|
||||||
self.private_config = None
|
self.private_config = None
|
||||||
self.auth_code_gen = AuthCodeGenerator.get_instance()
|
self.auth_code_gen = AuthCodeGenerator.get_instance()
|
||||||
self.is_device_verified = False # 添加设备验证状态标志
|
self.is_device_verified = False # 添加设备验证状态标志
|
||||||
|
self.music_handler = _music
|
||||||
|
|
||||||
async def handle_connection(self, ws):
|
async def handle_connection(self, ws):
|
||||||
try:
|
try:
|
||||||
# 获取并验证headers
|
# 获取并验证headers
|
||||||
self.headers = dict(ws.request.headers)
|
self.headers = dict(ws.request.headers)
|
||||||
self.logger.bind(tag=TAG).info(f"New connection request - Headers: {self.headers}")
|
# 获取客户端ip地址
|
||||||
|
client_ip = ws.remote_address[0]
|
||||||
|
self.logger.bind(tag=TAG).info(f"{client_ip} conn - Headers: {self.headers}")
|
||||||
|
|
||||||
# 进行认证
|
# 进行认证
|
||||||
await self.auth.authenticate(self.headers)
|
await self.auth.authenticate(self.headers)
|
||||||
|
|
||||||
device_id = self.headers.get("device-id", None)
|
device_id = self.headers.get("device-id", None)
|
||||||
|
|
||||||
# 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}")
|
||||||
@@ -104,10 +113,10 @@ class ConnectionHandler:
|
|||||||
# 判断是否已经绑定
|
# 判断是否已经绑定
|
||||||
owner = self.private_config.get_owner()
|
owner = self.private_config.get_owner()
|
||||||
self.is_device_verified = owner is not None
|
self.is_device_verified = owner is not None
|
||||||
|
|
||||||
if self.is_device_verified:
|
if self.is_device_verified:
|
||||||
await self.private_config.update_last_chat_time()
|
await self.private_config.update_last_chat_time()
|
||||||
|
|
||||||
llm, tts = self.private_config.create_private_instances()
|
llm, tts = self.private_config.create_private_instances()
|
||||||
if all([llm, tts]):
|
if all([llm, tts]):
|
||||||
self.llm = llm
|
self.llm = llm
|
||||||
@@ -146,7 +155,8 @@ class ConnectionHandler:
|
|||||||
await ws.close()
|
await ws.close()
|
||||||
return
|
return
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}")
|
stack_trace = traceback.format_exc()
|
||||||
|
self.logger.bind(tag=TAG).error(f"Connection error: {str(e)}-{stack_trace}")
|
||||||
await ws.close()
|
await ws.close()
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -166,7 +176,7 @@ class ConnectionHandler:
|
|||||||
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
date_time = time.strftime("%Y-%m-%d %H:%M", time.localtime())
|
||||||
self.prompt = self.prompt.replace("{date_time}", date_time)
|
self.prompt = self.prompt.replace("{date_time}", date_time)
|
||||||
self.dialogue.put(Message(role="system", content=self.prompt))
|
self.dialogue.put(Message(role="system", content=self.prompt))
|
||||||
|
|
||||||
async def _check_and_broadcast_auth_code(self):
|
async def _check_and_broadcast_auth_code(self):
|
||||||
"""检查设备绑定状态并广播认证码"""
|
"""检查设备绑定状态并广播认证码"""
|
||||||
if not self.private_config.get_owner():
|
if not self.private_config.get_owner():
|
||||||
@@ -186,7 +196,7 @@ class ConnectionHandler:
|
|||||||
# 如果不使用私有配置,就不需要验证
|
# 如果不使用私有配置,就不需要验证
|
||||||
return False
|
return False
|
||||||
return not self.is_device_verified
|
return not self.is_device_verified
|
||||||
|
|
||||||
def chat(self, query):
|
def chat(self, query):
|
||||||
# 如果设备未验证,就发送验证码
|
# 如果设备未验证,就发送验证码
|
||||||
if self.isNeedAuth():
|
if self.isNeedAuth():
|
||||||
@@ -199,7 +209,7 @@ class ConnectionHandler:
|
|||||||
finally:
|
finally:
|
||||||
loop.close()
|
loop.close()
|
||||||
return True
|
return True
|
||||||
|
|
||||||
self.dialogue.put(Message(role="user", content=query))
|
self.dialogue.put(Message(role="user", content=query))
|
||||||
response_message = []
|
response_message = []
|
||||||
start = 0
|
start = 0
|
||||||
@@ -316,6 +326,8 @@ class ConnectionHandler:
|
|||||||
|
|
||||||
async def close(self):
|
async def close(self):
|
||||||
"""资源清理方法"""
|
"""资源清理方法"""
|
||||||
|
|
||||||
|
# 清理其他资源
|
||||||
self.stop_event.set()
|
self.stop_event.set()
|
||||||
self.executor.shutdown(wait=False)
|
self.executor.shutdown(wait=False)
|
||||||
if self.websocket:
|
if self.websocket:
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import json
|
||||||
|
from config.logger import setup_logging
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
class IotDescriptor:
|
||||||
|
"""
|
||||||
|
A class to represent an IoT descriptor.
|
||||||
|
Attributes:
|
||||||
|
----------
|
||||||
|
name : str
|
||||||
|
The name of the IoT descriptor.
|
||||||
|
description : str
|
||||||
|
A brief description of the IoT descriptor.
|
||||||
|
properties : dict
|
||||||
|
A dictionary containing properties of the IoT descriptor.
|
||||||
|
methods : dict
|
||||||
|
A dictionary containing methods of the IoT descriptor.
|
||||||
|
-------
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, name, description, properties, methods):
|
||||||
|
self.name = name
|
||||||
|
self.description = description
|
||||||
|
self.properties = []
|
||||||
|
self.methods = []
|
||||||
|
|
||||||
|
# 根据描述创建属性
|
||||||
|
for key, value in properties.items():
|
||||||
|
# "volume":{"description":"当前音量 值","type":"number"}
|
||||||
|
"""
|
||||||
|
等价于
|
||||||
|
{
|
||||||
|
'name': 名字,
|
||||||
|
'description': 描述,
|
||||||
|
'value': 0
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# setattr(self, key, {}) # 创建一个空字典, 名字是属性名
|
||||||
|
property_item = globals()[key] = {} # 创建一个空字典, 名字是属性名
|
||||||
|
property_item['name'] = key
|
||||||
|
property_item["description"] = value["description"]
|
||||||
|
if value["type"] == "number":
|
||||||
|
property_item["value"] = 0
|
||||||
|
elif value["type"] == "boolean":
|
||||||
|
property_item["value"] = False
|
||||||
|
else:
|
||||||
|
property_item["value"] = ""
|
||||||
|
self.properties.append(property_item)
|
||||||
|
|
||||||
|
# 根据描述创建方法
|
||||||
|
for key, value in methods.items():
|
||||||
|
# "SetVolume": {"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}}}
|
||||||
|
"""
|
||||||
|
等价于
|
||||||
|
SetVolume = {
|
||||||
|
`description`: 描述,
|
||||||
|
`volume`: {
|
||||||
|
`description`: 描述,
|
||||||
|
`value`: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
# setattr(self, key, {}) # 创建一个空字典, 名字是方法名
|
||||||
|
method = globals()[key] = {} # 创建一个空字典, 名字是方法名
|
||||||
|
method["description"] = value["description"]
|
||||||
|
method['name'] = key
|
||||||
|
for k, v in value["parameters"].items():
|
||||||
|
# 不同的参数解析
|
||||||
|
method[k] = {}
|
||||||
|
method[k]["description"] = v["description"]
|
||||||
|
if v["type"] == "number":
|
||||||
|
method[k]["value"] = 0
|
||||||
|
elif v["type"] == "boolean":
|
||||||
|
method[k]["value"] = False
|
||||||
|
else:
|
||||||
|
method[k]["value"] = ""
|
||||||
|
|
||||||
|
self.methods.append(method)
|
||||||
|
|
||||||
|
|
||||||
|
async def handleIotDescriptors(conn, descriptors):
|
||||||
|
"""
|
||||||
|
处理物联网描述
|
||||||
|
示例: [{
|
||||||
|
"name":"Speaker",
|
||||||
|
"description":"当前 AI 机器人的扬声器",
|
||||||
|
"properties":{
|
||||||
|
"volume":{"description":"当前音量 值","type":"number"} 可以有boolean, number, string三种类型
|
||||||
|
},
|
||||||
|
"methods":{
|
||||||
|
"SetVolume":{
|
||||||
|
"description":"设置音量","parameters":{"volume":{"description":"0到100之间的整数","type":"number"}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}]
|
||||||
|
descriptors: 描述列表
|
||||||
|
"""
|
||||||
|
for descriptor in descriptors:
|
||||||
|
iot_descriptor = IotDescriptor(descriptor["name"], descriptor["description"], descriptor["properties"],
|
||||||
|
descriptor["methods"])
|
||||||
|
conn.iot_descriptors[descriptor["name"]] = iot_descriptor
|
||||||
|
|
||||||
|
# 暂时从配置文件中设置音量,后期通过意图识别控制音量
|
||||||
|
default_iot_volume = 100
|
||||||
|
if "iot" in conn.config:
|
||||||
|
default_iot_volume = conn.config["iot"]["Speaker"]["volume"]
|
||||||
|
logger.bind(tag=TAG).info(f"服务端设置音量为{default_iot_volume}")
|
||||||
|
await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": default_iot_volume})
|
||||||
|
|
||||||
|
|
||||||
|
async def send_iot_conn(conn, name, method_name, parameters):
|
||||||
|
"""
|
||||||
|
发送物联网指令
|
||||||
|
name: 设备名称 "Speaker"
|
||||||
|
method: 方法 "SetVolume"
|
||||||
|
parameters: 参数, 是一个字典 {"volume": 100}
|
||||||
|
发送示例:
|
||||||
|
{
|
||||||
|
"type": "iot",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name" : "Speaker",
|
||||||
|
"method": "SetVolume",
|
||||||
|
"parameters": {
|
||||||
|
"volume": 100
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
"""
|
||||||
|
|
||||||
|
for key, value in conn.iot_descriptors.items():
|
||||||
|
if key == name:
|
||||||
|
# 找到了设备
|
||||||
|
for method in value.methods:
|
||||||
|
# 找到了方法
|
||||||
|
if method["name"] == method_name:
|
||||||
|
await conn.websocket.send(json.dumps({
|
||||||
|
"type": "iot",
|
||||||
|
"commands": [
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"method": method_name,
|
||||||
|
"parameters": parameters
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}))
|
||||||
|
return
|
||||||
|
logger.bind(tag=TAG).error(f"未找到方法{method_name}")
|
||||||
@@ -0,0 +1,109 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
import os
|
||||||
|
import random
|
||||||
|
import difflib
|
||||||
|
import re
|
||||||
|
import traceback
|
||||||
|
from core.handle.sendAudioHandle import sendAudioMessage, send_stt_message
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
def _extract_song_name(text):
|
||||||
|
"""从用户输入中提取歌名"""
|
||||||
|
for keyword in ["听", "播放", "放", "唱"]:
|
||||||
|
if keyword in text:
|
||||||
|
parts = text.split(keyword)
|
||||||
|
if len(parts) > 1:
|
||||||
|
return parts[1].strip()
|
||||||
|
return None
|
||||||
|
|
||||||
|
|
||||||
|
def _find_best_match(potential_song, music_files):
|
||||||
|
"""查找最匹配的歌曲"""
|
||||||
|
best_match = None
|
||||||
|
highest_ratio = 0
|
||||||
|
|
||||||
|
for music_file in music_files:
|
||||||
|
song_name = os.path.splitext(music_file)[0]
|
||||||
|
ratio = difflib.SequenceMatcher(None, potential_song, song_name).ratio()
|
||||||
|
if ratio > highest_ratio and ratio > 0.4:
|
||||||
|
highest_ratio = ratio
|
||||||
|
best_match = music_file
|
||||||
|
return best_match
|
||||||
|
|
||||||
|
|
||||||
|
class MusicHandler:
|
||||||
|
def __init__(self, config):
|
||||||
|
self.config = config
|
||||||
|
self.music_related_keywords = []
|
||||||
|
|
||||||
|
if "music" in self.config:
|
||||||
|
self.music_config = self.config["music"]
|
||||||
|
self.music_dir = os.path.abspath(
|
||||||
|
self.music_config.get("music_dir", "./music") # 默认路径修改
|
||||||
|
)
|
||||||
|
self.music_related_keywords = self.music_config.get("music_commands", [])
|
||||||
|
else:
|
||||||
|
self.music_dir = os.path.abspath("./music")
|
||||||
|
self.music_related_keywords = ["来一首歌", "唱一首歌", "播放音乐", "来点音乐", "背景音乐", "放首歌",
|
||||||
|
"播放歌曲", "来点背景音乐", "我想听歌", "我要听歌", "放点音乐"]
|
||||||
|
|
||||||
|
async def handle_music_command(self, conn, text):
|
||||||
|
"""处理音乐播放指令"""
|
||||||
|
clean_text = re.sub(r'[^\w\s]', '', text).strip()
|
||||||
|
logger.bind(tag=TAG).debug(f"检查是否是音乐命令: {clean_text}")
|
||||||
|
|
||||||
|
# 尝试匹配具体歌名
|
||||||
|
if os.path.exists(self.music_dir):
|
||||||
|
music_files = [f for f in os.listdir(self.music_dir) if f.endswith('.mp3')]
|
||||||
|
logger.bind(tag=TAG).debug(f"找到的音乐文件: {music_files}")
|
||||||
|
|
||||||
|
potential_song = _extract_song_name(clean_text)
|
||||||
|
if potential_song:
|
||||||
|
best_match = _find_best_match(potential_song, music_files)
|
||||||
|
if best_match:
|
||||||
|
logger.bind(tag=TAG).info(f"找到最匹配的歌曲: {best_match}")
|
||||||
|
await self.play_local_music(conn, specific_file=best_match)
|
||||||
|
return True
|
||||||
|
|
||||||
|
# 检查是否是通用播放音乐命令
|
||||||
|
if any(cmd in clean_text for cmd in self.music_related_keywords):
|
||||||
|
await self.play_local_music(conn)
|
||||||
|
return True
|
||||||
|
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def play_local_music(self, conn, specific_file=None):
|
||||||
|
"""播放本地音乐文件"""
|
||||||
|
try:
|
||||||
|
if not os.path.exists(self.music_dir):
|
||||||
|
logger.bind(tag=TAG).error(f"音乐目录不存在: {self.music_dir}")
|
||||||
|
return
|
||||||
|
|
||||||
|
# 确保路径正确性
|
||||||
|
if specific_file:
|
||||||
|
music_path = os.path.join(self.music_dir, specific_file)
|
||||||
|
if not os.path.exists(music_path):
|
||||||
|
logger.bind(tag=TAG).error(f"指定的音乐文件不存在: {music_path}")
|
||||||
|
return
|
||||||
|
selected_music = specific_file
|
||||||
|
else:
|
||||||
|
music_files = [f for f in os.listdir(self.music_dir) if f.endswith('.mp3')]
|
||||||
|
if not music_files:
|
||||||
|
logger.bind(tag=TAG).error("未找到MP3音乐文件")
|
||||||
|
return
|
||||||
|
selected_music = random.choice(music_files)
|
||||||
|
music_path = os.path.join(self.music_dir, selected_music)
|
||||||
|
text = f"正在播放{selected_music}"
|
||||||
|
await send_stt_message(conn, text)
|
||||||
|
conn.tts_first_text = selected_music
|
||||||
|
conn.tts_last_text = selected_music
|
||||||
|
conn.llm_finish_task = True
|
||||||
|
opus_packets, duration = conn.tts.wav_to_opus_data(music_path)
|
||||||
|
await sendAudioMessage(conn, opus_packets, duration, selected_music)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.bind(tag=TAG).error(f"播放音乐失败: {str(e)}")
|
||||||
|
logger.bind(tag=TAG).error(f"详细错误: {traceback.format_exc()}")
|
||||||
@@ -0,0 +1,81 @@
|
|||||||
|
from config.logger import setup_logging
|
||||||
|
import asyncio
|
||||||
|
import time
|
||||||
|
from core.utils.util import remove_punctuation_and_length
|
||||||
|
from core.handle.sendAudioHandle import schedule_with_interrupt, send_stt_message
|
||||||
|
|
||||||
|
TAG = __name__
|
||||||
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
|
async def handleAudioMessage(conn, audio):
|
||||||
|
if not conn.asr_server_receive:
|
||||||
|
logger.bind(tag=TAG).debug(f"前期数据处理中,暂停接收")
|
||||||
|
return
|
||||||
|
if conn.client_listen_mode == "auto":
|
||||||
|
have_voice = conn.vad.is_vad(conn, audio)
|
||||||
|
else:
|
||||||
|
have_voice = conn.client_have_voice
|
||||||
|
|
||||||
|
# 如果本次没有声音,本段也没声音,就把声音丢弃了
|
||||||
|
if have_voice == False and conn.client_have_voice == False:
|
||||||
|
await no_voice_close_connect(conn)
|
||||||
|
conn.asr_audio.clear()
|
||||||
|
return
|
||||||
|
conn.client_no_voice_last_time = 0.0
|
||||||
|
conn.asr_audio.append(audio)
|
||||||
|
# 如果本段有声音,且已经停止了
|
||||||
|
if conn.client_voice_stop:
|
||||||
|
conn.client_abort = False
|
||||||
|
conn.asr_server_receive = False
|
||||||
|
# 音频太短了,无法识别
|
||||||
|
if len(conn.asr_audio) < 3:
|
||||||
|
conn.asr_server_receive = True
|
||||||
|
else:
|
||||||
|
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
||||||
|
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
||||||
|
text_len, text_without_punctuation = remove_punctuation_and_length(text)
|
||||||
|
if await conn.music_handler.handle_music_command(conn, text_without_punctuation):
|
||||||
|
conn.asr_server_receive = True
|
||||||
|
conn.asr_audio.clear()
|
||||||
|
return
|
||||||
|
if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
|
||||||
|
return
|
||||||
|
if text_len > 0:
|
||||||
|
await startToChat(conn, text)
|
||||||
|
else:
|
||||||
|
conn.asr_server_receive = True
|
||||||
|
conn.asr_audio.clear()
|
||||||
|
conn.reset_vad_states()
|
||||||
|
|
||||||
|
|
||||||
|
async def handleCMDMessage(conn, text):
|
||||||
|
cmd_exit = conn.cmd_exit
|
||||||
|
for cmd in cmd_exit:
|
||||||
|
if text == cmd:
|
||||||
|
logger.bind(tag=TAG).info("识别到明确的退出命令".format(text))
|
||||||
|
await conn.close()
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
async def startToChat(conn, text):
|
||||||
|
# 异步发送 stt 信息
|
||||||
|
stt_task = asyncio.create_task(
|
||||||
|
schedule_with_interrupt(0, send_stt_message(conn, text))
|
||||||
|
)
|
||||||
|
conn.scheduled_tasks.append(stt_task)
|
||||||
|
conn.executor.submit(conn.chat, text)
|
||||||
|
|
||||||
|
|
||||||
|
async def no_voice_close_connect(conn):
|
||||||
|
if conn.client_no_voice_last_time == 0.0:
|
||||||
|
conn.client_no_voice_last_time = time.time() * 1000
|
||||||
|
else:
|
||||||
|
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)
|
||||||
|
if no_voice_time > 1000 * close_connection_no_voice_time:
|
||||||
|
conn.client_abort = False
|
||||||
|
conn.asr_server_receive = False
|
||||||
|
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜”为结尾"
|
||||||
|
await startToChat(conn, prompt)
|
||||||
@@ -8,56 +8,6 @@ TAG = __name__
|
|||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
|
|
||||||
|
|
||||||
async def handleAudioMessage(conn, audio):
|
|
||||||
if not conn.asr_server_receive:
|
|
||||||
logger.bind(tag=TAG).debug(f"前期数据处理中,暂停接收")
|
|
||||||
return
|
|
||||||
if conn.client_listen_mode == "auto":
|
|
||||||
have_voice = conn.vad.is_vad(conn, audio)
|
|
||||||
else:
|
|
||||||
have_voice = conn.client_have_voice
|
|
||||||
|
|
||||||
# 如果本次没有声音,本段也没声音,就把声音丢弃了
|
|
||||||
if have_voice == False and conn.client_have_voice == False:
|
|
||||||
await no_voice_close_connect(conn)
|
|
||||||
conn.asr_audio.clear()
|
|
||||||
return
|
|
||||||
conn.client_no_voice_last_time = 0.0
|
|
||||||
conn.asr_audio.append(audio)
|
|
||||||
# 如果本段有声音,且已经停止了
|
|
||||||
if conn.client_voice_stop:
|
|
||||||
conn.client_abort = False
|
|
||||||
conn.asr_server_receive = False
|
|
||||||
# 音频太短了,无法识别
|
|
||||||
if len(conn.asr_audio) < 3:
|
|
||||||
conn.asr_server_receive = True
|
|
||||||
else:
|
|
||||||
text, file_path = await conn.asr.speech_to_text(conn.asr_audio, conn.session_id)
|
|
||||||
logger.bind(tag=TAG).info(f"识别文本: {text}")
|
|
||||||
text_len, text_without_punctuation = remove_punctuation_and_length(text)
|
|
||||||
if text_len <= conn.max_cmd_length and await handleCMDMessage(conn, text_without_punctuation):
|
|
||||||
return
|
|
||||||
if text_len > 0:
|
|
||||||
await startToChat(conn, text)
|
|
||||||
else:
|
|
||||||
conn.asr_server_receive = True
|
|
||||||
conn.asr_audio.clear()
|
|
||||||
conn.reset_vad_states()
|
|
||||||
|
|
||||||
async def handleCMDMessage(conn, text):
|
|
||||||
cmd_exit = conn.cmd_exit
|
|
||||||
for cmd in cmd_exit:
|
|
||||||
if text == cmd:
|
|
||||||
logger.bind(tag=TAG).info("识别到明确的退出命令".format(text))
|
|
||||||
await finishToChat(conn)
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
|
|
||||||
|
|
||||||
async def finishToChat(conn):
|
|
||||||
await conn.close()
|
|
||||||
|
|
||||||
|
|
||||||
async def isLLMWantToFinish(conn):
|
async def isLLMWantToFinish(conn):
|
||||||
first_text = conn.tts_first_text
|
first_text = conn.tts_first_text
|
||||||
last_text = conn.tts_last_text
|
last_text = conn.tts_last_text
|
||||||
@@ -70,15 +20,6 @@ async def isLLMWantToFinish(conn):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
async def startToChat(conn, text):
|
|
||||||
# 异步发送 stt 信息
|
|
||||||
stt_task = asyncio.create_task(
|
|
||||||
schedule_with_interrupt(0, send_stt_message(conn, text))
|
|
||||||
)
|
|
||||||
conn.scheduled_tasks.append(stt_task)
|
|
||||||
conn.executor.submit(conn.chat, text)
|
|
||||||
|
|
||||||
|
|
||||||
async def sendAudioMessage(conn, audios, duration, text):
|
async def sendAudioMessage(conn, audios, duration, text):
|
||||||
base_delay = conn.tts_duration
|
base_delay = conn.tts_duration
|
||||||
|
|
||||||
@@ -107,7 +48,7 @@ async def sendAudioMessage(conn, audios, duration, text):
|
|||||||
conn.scheduled_tasks.append(stop_task)
|
conn.scheduled_tasks.append(stop_task)
|
||||||
if await isLLMWantToFinish(conn):
|
if await isLLMWantToFinish(conn):
|
||||||
finish_task = asyncio.create_task(
|
finish_task = asyncio.create_task(
|
||||||
schedule_with_interrupt(stop_duration, finishToChat(conn))
|
schedule_with_interrupt(stop_duration, await conn.close())
|
||||||
)
|
)
|
||||||
conn.scheduled_tasks.append(finish_task)
|
conn.scheduled_tasks.append(finish_task)
|
||||||
|
|
||||||
@@ -152,16 +93,3 @@ async def schedule_with_interrupt(delay, coro):
|
|||||||
await coro
|
await coro
|
||||||
except asyncio.CancelledError:
|
except asyncio.CancelledError:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
async def no_voice_close_connect(conn):
|
|
||||||
if conn.client_no_voice_last_time == 0.0:
|
|
||||||
conn.client_no_voice_last_time = time.time() * 1000
|
|
||||||
else:
|
|
||||||
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)
|
|
||||||
if no_voice_time > 1000 * close_connection_no_voice_time:
|
|
||||||
conn.client_abort = False
|
|
||||||
conn.asr_server_receive = False
|
|
||||||
prompt = "时间过得真快,我都好久没说话了。请你用十个字左右话跟我告别,以“再见”或“拜拜拜”为结尾"
|
|
||||||
await startToChat(conn, prompt)
|
|
||||||
@@ -2,7 +2,8 @@ from config.logger import setup_logging
|
|||||||
import json
|
import json
|
||||||
from core.handle.abortHandle import handleAbortMessage
|
from core.handle.abortHandle import handleAbortMessage
|
||||||
from core.handle.helloHandle import handleHelloMessage
|
from core.handle.helloHandle import handleHelloMessage
|
||||||
from core.handle.audioHandle import startToChat
|
from core.handle.receiveAudioHandle import startToChat
|
||||||
|
from core.handle.iotHandle import handleIotDescriptors
|
||||||
|
|
||||||
TAG = __name__
|
TAG = __name__
|
||||||
logger = setup_logging()
|
logger = setup_logging()
|
||||||
@@ -36,5 +37,8 @@ async def handleTextMessage(conn, message):
|
|||||||
conn.asr_audio.clear()
|
conn.asr_audio.clear()
|
||||||
if "text" in msg_json:
|
if "text" in msg_json:
|
||||||
await startToChat(conn, msg_json["text"])
|
await startToChat(conn, msg_json["text"])
|
||||||
|
elif msg_json["type"] == "iot":
|
||||||
|
if "descriptors" in msg_json:
|
||||||
|
await handleIotDescriptors(conn, msg_json["descriptors"])
|
||||||
except json.JSONDecodeError:
|
except json.JSONDecodeError:
|
||||||
await conn.websocket.send(message)
|
await conn.websocket.send(message)
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import websockets
|
|||||||
import json
|
import json
|
||||||
import gzip
|
import gzip
|
||||||
|
|
||||||
import opuslib
|
import opuslib_next
|
||||||
from core.providers.asr.base import ASRProviderBase
|
from core.providers.asr.base import ASRProviderBase
|
||||||
|
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
@@ -103,14 +103,14 @@ class ASRProvider(ASRProviderBase):
|
|||||||
file_name = f"asr_{session_id}_{uuid.uuid4()}.wav"
|
file_name = f"asr_{session_id}_{uuid.uuid4()}.wav"
|
||||||
file_path = os.path.join(self.output_dir, file_name)
|
file_path = os.path.join(self.output_dir, file_name)
|
||||||
|
|
||||||
decoder = opuslib.Decoder(16000, 1) # 16kHz, 单声道
|
decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道
|
||||||
pcm_data = []
|
pcm_data = []
|
||||||
|
|
||||||
for opus_packet in opus_data:
|
for opus_packet in opus_data:
|
||||||
try:
|
try:
|
||||||
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
||||||
pcm_data.append(pcm_frame)
|
pcm_data.append(pcm_frame)
|
||||||
except opuslib.OpusError as e:
|
except opuslib_next.OpusError as e:
|
||||||
logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
||||||
|
|
||||||
with wave.open(file_path, "wb") as wf:
|
with wave.open(file_path, "wb") as wf:
|
||||||
@@ -216,14 +216,14 @@ class ASRProvider(ASRProviderBase):
|
|||||||
@staticmethod
|
@staticmethod
|
||||||
def decode_opus(opus_data: List[bytes], session_id: str) -> List[bytes]:
|
def decode_opus(opus_data: List[bytes], session_id: str) -> List[bytes]:
|
||||||
|
|
||||||
decoder = opuslib.Decoder(16000, 1) # 16kHz, 单声道
|
decoder = opuslib_next.Decoder(16000, 1) # 16kHz, 单声道
|
||||||
pcm_data = []
|
pcm_data = []
|
||||||
|
|
||||||
for opus_packet in opus_data:
|
for opus_packet in opus_data:
|
||||||
try:
|
try:
|
||||||
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
pcm_frame = decoder.decode(opus_packet, 960) # 960 samples = 60ms
|
||||||
pcm_data.append(pcm_frame)
|
pcm_data.append(pcm_frame)
|
||||||
except opuslib.OpusError as e:
|
except opuslib_next.OpusError as e:
|
||||||
logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
logger.bind(tag=TAG).error(f"Opus解码错误: {e}", exc_info=True)
|
||||||
|
|
||||||
return pcm_data
|
return pcm_data
|
||||||
|
|||||||
@@ -25,12 +25,25 @@ class LLMProvider(LLMProviderBase):
|
|||||||
messages=dialogue,
|
messages=dialogue,
|
||||||
stream=True
|
stream=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
is_active = True
|
||||||
for chunk in responses:
|
for chunk in responses:
|
||||||
# 检查是否存在有效的choice且content不为空
|
try:
|
||||||
if chunk.choices and len(chunk.choices) > 0:
|
# 检查是否存在有效的choice且content不为空
|
||||||
delta = chunk.choices[0].delta
|
delta = chunk.choices[0].delta if getattr(chunk, 'choices', None) else None
|
||||||
content = getattr(delta, 'content', '')
|
content = delta.content if hasattr(delta, 'content') else ''
|
||||||
if content: # 仅在content非空时生成
|
except IndexError:
|
||||||
|
content = ''
|
||||||
|
if content:
|
||||||
|
# 处理标签跨多个chunk的情况
|
||||||
|
if '<think>' in content:
|
||||||
|
is_active = False
|
||||||
|
content = content.split('<think>')[0]
|
||||||
|
if '</think>' in content:
|
||||||
|
is_active = True
|
||||||
|
content = content.split('</think>')[-1]
|
||||||
|
if is_active:
|
||||||
yield content
|
yield content
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
logger.bind(tag=TAG).error(f"Error in response generation: {e}")
|
||||||
|
|||||||
+2
-2
@@ -90,7 +90,7 @@ def remove_punctuation_and_length(text):
|
|||||||
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])
|
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":
|
if result == "Yeah":
|
||||||
return 0
|
return 0, ""
|
||||||
return len(result), result
|
return len(result), result
|
||||||
|
|
||||||
|
|
||||||
@@ -120,4 +120,4 @@ def check_password(password):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
# 如果满足所有条件,则返回True
|
# 如果满足所有条件,则返回True
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import asyncio
|
|||||||
import websockets
|
import websockets
|
||||||
from config.logger import setup_logging
|
from config.logger import setup_logging
|
||||||
from core.connection import ConnectionHandler
|
from core.connection import ConnectionHandler
|
||||||
|
from core.handle.musicHandler import MusicHandler
|
||||||
from core.utils.util import get_local_ip
|
from core.utils.util import get_local_ip
|
||||||
from core.utils import asr, vad, llm, tts
|
from core.utils import asr, vad, llm, tts
|
||||||
|
|
||||||
@@ -12,7 +13,8 @@ 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._create_processing_instances()
|
self._vad, self._asr, self._llm, self._tts, self._music = self._create_processing_instances()
|
||||||
|
self.active_connections = set() # 添加全局连接记录
|
||||||
|
|
||||||
def _create_processing_instances(self):
|
def _create_processing_instances(self):
|
||||||
"""创建处理模块实例"""
|
"""创建处理模块实例"""
|
||||||
@@ -43,7 +45,8 @@ class WebSocketServer:
|
|||||||
self.config["TTS"][self.config["selected_module"]["TTS"]]["type"],
|
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"]
|
||||||
)
|
),
|
||||||
|
MusicHandler(self.config)
|
||||||
)
|
)
|
||||||
|
|
||||||
async def start(self):
|
async def start(self):
|
||||||
@@ -62,5 +65,10 @@ class WebSocketServer:
|
|||||||
|
|
||||||
async def _handle_connection(self, websocket):
|
async def _handle_connection(self, websocket):
|
||||||
"""处理新连接,每次创建独立的ConnectionHandler"""
|
"""处理新连接,每次创建独立的ConnectionHandler"""
|
||||||
handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts)
|
# 创建ConnectionHandler时传入当前server实例
|
||||||
await handler.handle_connection(websocket)
|
handler = ConnectionHandler(self.config, self._vad, self._asr, self._llm, self._tts, self._music)
|
||||||
|
self.active_connections.add(handler)
|
||||||
|
try:
|
||||||
|
await handler.handle_connection(websocket)
|
||||||
|
finally:
|
||||||
|
self.active_connections.discard(handler)
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
version: '3'
|
||||||
services:
|
services:
|
||||||
xiaozhi-esp32-server:
|
xiaozhi-esp32-server:
|
||||||
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:latest
|
||||||
|
|||||||
+1
-1
@@ -117,7 +117,7 @@ docker run -it --name xiaozhi-env --restart always --security-opt seccomp:unconf
|
|||||||
-p 8000:8000 \
|
-p 8000:8000 \
|
||||||
-p 8002:8002 \
|
-p 8002:8002 \
|
||||||
-v ./:/app \
|
-v ./:/app \
|
||||||
ccr.ccs.tencentyun.com/kalicyh/poetry:v3.10_latest
|
kalicyh/poetry:v3.10_xiaozhi
|
||||||
```
|
```
|
||||||
|
|
||||||
然后就和正常开发一样了
|
然后就和正常开发一样了
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 446 KiB After Width: | Height: | Size: 427 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
+1
-1
@@ -16,4 +16,4 @@ aiohttp_cors==0.7.0
|
|||||||
ormsgpack==1.7.0
|
ormsgpack==1.7.0
|
||||||
ruamel.yaml==0.18.10
|
ruamel.yaml==0.18.10
|
||||||
loguru==0.7.3
|
loguru==0.7.3
|
||||||
requests>=2.0.0
|
requests==2.32.3
|
||||||
Reference in New Issue
Block a user