diff --git a/README.md b/README.md
index 121dfbe6..cdf31847 100644
--- a/README.md
+++ b/README.md
@@ -57,6 +57,13 @@
+
+
+
+
+
+
+ |
|
@@ -74,9 +81,16 @@
|
-
+
-
+
+
+
+ |
+
+
+
+
|
@@ -143,12 +157,13 @@ server:
支持 EdgeTTS(默认)、火山引擎豆包 TTS 等多种 TTS 接口,满足语音合成需求。
- **记忆功能**
支持超长记忆、本地总结记忆、无记忆三种模式,满足不同场景需求。
+- **IOT功能**
+ 支持管理注册设备IOT功能,支持基于对话上下文语境下的智能物联网控制。
### 正在开发 🚧
- 多种心情模式
- 智控台webui
-- iot功能
想了解具体开发进度,[请点击这里](https://github.com/users/xinnan-tech/projects/3)
diff --git a/docs/contributor_open_letter.md b/docs/contributor_open_letter.md
index 180eef02..7a547197 100644
--- a/docs/contributor_open_letter.md
+++ b/docs/contributor_open_letter.md
@@ -35,7 +35,7 @@ John,2025.3.11,广州
### 1、成为普通贡献者
-Fork 项目,,提交 PR,由开发者审核后合入主分支。
+Fork 项目,提交 PR,由开发者审核后合入主分支。
### 2、成为开发者
diff --git a/docs/images/demo8.png b/docs/images/demo8.png
new file mode 100644
index 00000000..affe6ce2
Binary files /dev/null and b/docs/images/demo8.png differ
diff --git a/docs/images/demo9.png b/docs/images/demo9.png
new file mode 100644
index 00000000..74b6e463
Binary files /dev/null and b/docs/images/demo9.png differ
diff --git a/main/xiaozhi-server/config.yaml b/main/xiaozhi-server/config.yaml
index 89a6f7f9..36e660f7 100644
--- a/main/xiaozhi-server/config.yaml
+++ b/main/xiaozhi-server/config.yaml
@@ -35,10 +35,7 @@ log:
log_file: "server.log"
# 设置数据文件路径
data_dir: data
-iot:
- Speaker:
- # 设置esp32的音量,范围0-100
- volume: 80
+
xiaozhi:
type: hello
version: 1
@@ -117,10 +114,10 @@ Memory:
# 每月有1000次免费调用
api_key: 你的mem0ai api key
nomem:
- # 不想使用记忆功能,可以使用nomem
+ # 不想使用记忆功能,可以使用nomem
type: nomem
mem_local_short:
- # 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器
+ # 本地记忆功能,通过selected_module的llm总结,数据保存在本地,不会上传到服务器
type: mem_local_short
ASR:
@@ -232,7 +229,7 @@ TTS:
# 定义TTS API类型
type: edge
voice: zh-CN-XiaoxiaoNeural
- output_file: tmp/
+ output_dir: tmp/
DoubaoTTS:
# 定义TTS API类型
type: doubao
@@ -242,7 +239,7 @@ TTS:
# 地址:https://console.volcengine.com/speech/service/8
api_url: https://openspeech.bytedance.com/api/v1/tts
voice: BV001_streaming
- output_file: tmp/
+ output_dir: tmp/
authorization: "Bearer;"
appid: 你的火山引擎语音合成服务appid
access_token: 你的火山引擎语音合成服务access_token
@@ -253,7 +250,7 @@ TTS:
# token申请地址 https://cloud.siliconflow.cn/account/ak
model: FunAudioLLM/CosyVoice2-0.5B
voice: FunAudioLLM/CosyVoice2-0.5B:alex
- output_file: tmp/
+ output_dir: tmp/
access_token: 你的硅基流动API密钥
response_format: wav
CozeCnTTS:
@@ -261,7 +258,7 @@ TTS:
# COZECN TTS
# token申请地址 https://www.coze.cn/open/oauth/pats
voice: 7426720361733046281
- output_file: tmp/
+ output_dir: tmp/
access_token: 你的coze web key
response_format: wav
FishSpeech:
@@ -274,7 +271,7 @@ TTS:
#--decoder-config-name firefly_gan_vq
#--compile
type: fishspeech
- output_file: tmp/
+ output_dir: tmp/
response_format: wav
reference_id: null
reference_audio: ["/tmp/test.wav",]
@@ -298,7 +295,7 @@ TTS:
#python api_v2.py -a 127.0.0.1 -p 9880 -c GPT_SoVITS/configs/caixukun.yaml
type: gpt_sovits_v2
url: "http://127.0.0.1:9880/tts"
- output_file: tmp/
+ output_dir: tmp/
text_lang: "auto"
ref_audio_path: "caixukun.wav"
prompt_text: ""
@@ -323,7 +320,7 @@ TTS:
#python api.py
type: gpt_sovits_v3
url: "http://127.0.0.1:9880"
- output_file: tmp/
+ output_dir: tmp/
text_language: "auto"
refer_wav_path: "caixukun.wav"
prompt_language: "zh"
@@ -344,7 +341,7 @@ TTS:
# api_key地址:https://platform.minimaxi.com/user-center/basic-information/interface-key
# 定义TTS API类型
type: minimax
- output_file: tmp/
+ output_dir: tmp/
group_id: 你的minimax平台groupID
api_key: 你的minimax平台接口密钥
model: "speech-01-turbo"
@@ -381,7 +378,7 @@ TTS:
# token地址:https://nls-portal.console.aliyun.com/overview
# 定义TTS API类型
type: aliyun
- output_file: tmp/
+ output_dir: tmp/
appkey: 你的阿里云智能语音交互服务项目Appkey
token: 你的阿里云智能语音交互服务AccessToken,临时的24小时,要长期用下方的access_key_id,access_key_secret
voice: xiaoyun
@@ -404,7 +401,7 @@ TTS:
api_url: https://api.302ai.cn/doubao/tts_hd
authorization: "Bearer "
voice: "zh_female_wanwanxiaohe_moon_bigtts"
- output_file: tmp/
+ output_dir: tmp/
access_token: "你的302API密钥"
ACGNTTS:
#在线网址:https://acgn.ttson.cn/
@@ -421,7 +418,7 @@ TTS:
to_lang: ZH
url: https://u95167-bd74-2aef8085.westx.seetacloud.com:8443/flashsummary/tts?token=
format: mp3
- output_file: tmp/
+ output_dir: tmp/
emotion: 1
OpenAITTS:
# openai官方文本转语音服务,可支持全球大多数语种
@@ -435,7 +432,7 @@ TTS:
voice: onyx
# 语速范围0.25-4.0
speed: 1
- output_file: tmp/
+ output_dir: tmp/
CustomTTS:
# 自定义的TTS接口服务,请求参数可自定义
# 要求接口使用GET方式请求,并返回音频文件
@@ -450,7 +447,7 @@ TTS:
headers: # 自定义请求头
# Authorization: Bearer xxxx
format: wav # 接口返回的音频格式
- output_file: tmp/
+ output_dir: tmp/
# 模块测试配置
module_test:
test_sentences: # 自定义测试语句
@@ -473,4 +470,4 @@ manager:
enabled: false
ip: 0.0.0.0
port: 8002
-use_private_config: false
\ No newline at end of file
+use_private_config: false
diff --git a/main/xiaozhi-server/config/settings.py b/main/xiaozhi-server/config/settings.py
index 0582bf76..bb81cfe9 100644
--- a/main/xiaozhi-server/config/settings.py
+++ b/main/xiaozhi-server/config/settings.py
@@ -7,9 +7,48 @@ from core.utils.util import read_config, get_project_dir
default_config_file = "config.yaml"
+def ensure_directories(config):
+ """确保所有配置路径存在"""
+ dirs_to_create = set()
+ project_dir = get_project_dir() # 获取项目根目录
+ # 日志文件目录
+ log_dir = config.get('log', {}).get('log_dir', 'tmp')
+ dirs_to_create.add(os.path.join(project_dir, log_dir))
+
+ # ASR/TTS模块输出目录
+ for module in ['ASR', 'TTS']:
+ for provider in config.get(module, {}).values():
+ output_dir = provider.get('output_dir', '')
+ if output_dir:
+ dirs_to_create.add(output_dir)
+
+ # 根据selected_module创建模型目录
+ selected_modules = config.get('selected_module', {})
+ for module_type in ['ASR', 'LLM', 'TTS']:
+ selected_provider = selected_modules.get(module_type)
+ if not selected_provider:
+ continue
+ provider_config = config.get(module_type, {}).get(selected_provider, {})
+ output_dir = provider_config.get('output_dir')
+ if output_dir:
+ full_model_dir = os.path.join(project_dir, output_dir)
+ dirs_to_create.add(full_model_dir)
+
+ # 统一创建目录(保留原data目录创建)
+ for dir_path in dirs_to_create:
+ try:
+ os.makedirs(dir_path, exist_ok=True)
+ except PermissionError:
+ print(f"警告:无法创建目录 {dir_path},请检查写入权限")
+
+
def get_config_file():
global default_config_file
- # 判断是否存在私有的配置文件
+ """获取配置文件路径,优先使用私有配置文件(若存在)。
+
+ Returns:
+ str: 配置文件路径(相对路径或默认路径)
+ """
config_file = default_config_file
if os.path.exists(get_project_dir() + "data/." + default_config_file):
config_file = "data/." + default_config_file
@@ -20,9 +59,13 @@ def load_config():
"""加载配置文件"""
parser = argparse.ArgumentParser(description="Server configuration")
config_file = get_config_file()
+
parser.add_argument("--config_path", type=str, default=config_file)
args = parser.parse_args()
- return read_config(args.config_path)
+ config = read_config(args.config_path)
+ # 初始化目录
+ ensure_directories(config)
+ return config
def update_config(config):
@@ -67,7 +110,7 @@ def find_missing_keys(new_config, old_config, parent_key=''):
def check_config_file():
old_config_file = get_config_file()
global default_config_file
- if not old_config_file.startswith('data'):
+ if not 'data' in old_config_file:
return
old_config = read_config(get_project_dir() + old_config_file)
new_config = read_config(get_project_dir() + default_config_file)
diff --git a/main/xiaozhi-server/core/connection.py b/main/xiaozhi-server/core/connection.py
index adb52c1f..718398c3 100644
--- a/main/xiaozhi-server/core/connection.py
+++ b/main/xiaozhi-server/core/connection.py
@@ -197,7 +197,7 @@ class ConnectionHandler:
self.dialogue.put(Message(role="system", content=self.prompt))
self.func_handler = FunctionHandler(self.config)
-
+
def change_system_prompt(self, prompt):
self.prompt = prompt
# 找到原来的role==system,替换原来的系统提示
@@ -303,7 +303,7 @@ class ConnectionHandler:
self.logger.bind(tag=TAG).debug(json.dumps(self.dialogue.get_llm_dialogue(), indent=4, ensure_ascii=False))
return True
- def chat_with_function_calling(self, query, tool_call = False):
+ def chat_with_function_calling(self, query, tool_call=False):
self.logger.bind(tag=TAG).debug(f"Chat with function calling start: {query}")
"""Chat with function calling for intent detection using streaming"""
if self.isNeedAuth():
@@ -311,7 +311,7 @@ class ConnectionHandler:
future = asyncio.run_coroutine_threadsafe(self._check_and_broadcast_auth_code(), self.loop)
future.result()
return True
-
+
if not tool_call:
self.dialogue.put(Message(role="user", content=query))
@@ -320,7 +320,7 @@ class ConnectionHandler:
response_message = []
processed_chars = 0 # 跟踪已处理的字符位置
-
+
try:
start_time = time.time()
@@ -328,7 +328,7 @@ class ConnectionHandler:
future = asyncio.run_coroutine_threadsafe(self.memory.query_memory(query), self.loop)
memory_str = future.result()
- #self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
+ # self.logger.bind(tag=TAG).info(f"对话记录: {self.dialogue.get_llm_dialogue_with_memory(memory_str)}")
# 使用支持functions的streaming接口
llm_responses = self.llm.response_with_functions(
@@ -351,8 +351,8 @@ class ConnectionHandler:
content_arguments = ""
for response in llm_responses:
content, tools_call = response
- if content is not None and len(content)>0:
- if len(response_message)<=0 and (content=="```" or "" in content):
+ if content is not None and len(content) > 0:
+ if len(response_message) <= 0 and (content == "```" or "" in content):
tool_call_flag = True
if tools_call is not None:
@@ -366,7 +366,7 @@ class ConnectionHandler:
if content is not None and len(content) > 0:
if tool_call_flag:
- content_arguments+=content
+ content_arguments += content
else:
response_message.append(content)
@@ -422,16 +422,17 @@ class ConnectionHandler:
else:
function_arguments = json.loads(function_arguments)
if not bHasError:
- self.logger.bind(tag=TAG).info(f"function_name={function_name}, function_id={function_id}, function_arguments={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 = self.func_handler.handle_llm_function_call(self, function_call_data)
- self._handle_function_result(result, function_call_data, text_index+1)
+ self._handle_function_result(result, function_call_data, text_index + 1)
- # 处理最后剩余的文本
+ # 处理最后剩余的文本
full_text = "".join(response_message)
remaining_text = full_text[processed_chars:]
if remaining_text:
@@ -443,7 +444,7 @@ class ConnectionHandler:
self.tts_queue.put(future)
# 存储对话内容
- if len(response_message)>0:
+ if len(response_message) > 0:
self.dialogue.put(Message(role="assistant", content="".join(response_message)))
self.llm_finish_task = True
@@ -452,31 +453,40 @@ class ConnectionHandler:
return True
def _handle_function_result(self, result, function_call_data, text_index):
- if result.action == Action.RESPONSE: # 直接回复前端
+ 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生成回复
-
+ elif result.action == Action.REQLLM: # 调用函数后再请求llm生成回复
+
text = result.result
if text is not None and len(text) > 0:
function_id = function_call_data["id"]
function_name = function_call_data["name"]
function_arguments = function_call_data["arguments"]
self.dialogue.put(Message(role='assistant',
- tool_calls=[{"id": function_id,
- "function": {"arguments": function_arguments,"name": function_name},
- "type": 'function',
- "index": 0}]))
+ tool_calls=[{"id": function_id,
+ "function": {"arguments": function_arguments,
+ "name": function_name},
+ "type": 'function',
+ "index": 0}]))
self.dialogue.put(Message(role="tool", tool_call_id=function_id, content=text))
self.chat_with_function_calling(text, tool_call=True)
- if result.action == Action.NOTFOUND:
- text = result.response
-
-
+ elif result.action == Action.NOTFOUND:
+ text = result.result
+ 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))
+ else:
+ text = result.result
+ 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))
def _tts_priority_thread(self):
while not self.stop_event.is_set():
diff --git a/main/xiaozhi-server/core/handle/functionHandler.py b/main/xiaozhi-server/core/handle/functionHandler.py
index 8d54fe8a..c7449973 100644
--- a/main/xiaozhi-server/core/handle/functionHandler.py
+++ b/main/xiaozhi-server/core/handle/functionHandler.py
@@ -50,6 +50,7 @@ class FunctionHandler:
self.function_registry.register_function("play_music")
self.function_registry.register_function("plugin_loader")
self.function_registry.register_function("get_time")
+ self.function_registry.register_function("raise_and_lower_the_volume")
def register_config_functions(self):
"""注册配置中的函数,可以不同客户端使用不同的配置"""
@@ -69,7 +70,7 @@ class FunctionHandler:
arguments = function_call_data["arguments"]
arguments = json.loads(arguments) if arguments else {}
logger.bind(tag=TAG).info(f"调用函数: {function_name}, 参数: {arguments}")
- if funcItem.type == ToolType.SYSTEM_CTL:
+ if funcItem.type == ToolType.SYSTEM_CTL or funcItem.type == ToolType.IOT_CTL:
return func(conn, **arguments)
elif funcItem.type == ToolType.WAIT:
return func(**arguments)
diff --git a/main/xiaozhi-server/core/handle/iotHandle.py b/main/xiaozhi-server/core/handle/iotHandle.py
index 05d32d5a..7de2ef57 100644
--- a/main/xiaozhi-server/core/handle/iotHandle.py
+++ b/main/xiaozhi-server/core/handle/iotHandle.py
@@ -1,24 +1,151 @@
import json
+import asyncio
from config.logger import setup_logging
+from plugins_func.register import device_type_registry, register_function, ActionResponse, Action, ToolType
TAG = __name__
logger = setup_logging()
+def wrap_async_function(async_func):
+ """包装异步函数为同步函数"""
+
+ def wrapper(*args, **kwargs):
+ try:
+ # 获取连接对象(第一个参数)
+ conn = args[0]
+ if not hasattr(conn, 'loop'):
+ logger.bind(tag=TAG).error("Connection对象没有loop属性")
+ return ActionResponse(Action.ERROR, "Connection对象没有loop属性",
+ "执行操作时出错: Connection对象没有loop属性")
+
+ # 使用conn对象中的事件循环
+ loop = conn.loop
+ # 在conn的事件循环中运行异步函数
+ future = asyncio.run_coroutine_threadsafe(async_func(*args, **kwargs), loop)
+ # 等待结果返回
+ return future.result()
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"运行异步函数时出错: {e}")
+ return ActionResponse(Action.ERROR, str(e), f"执行操作时出错: {e}")
+
+ return wrapper
+
+
+def create_iot_function(device_name, method_name, method_info):
+ """
+ 根据IOT设备描述生成通用的控制函数
+ """
+
+ async def iot_control_function(conn, response_success=None, response_failure=None, **params):
+ try:
+ # 打印响应参数
+ logger.bind(tag=TAG).info(
+ f"控制函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
+
+ # 发送控制命令
+ await send_iot_conn(conn, device_name, method_name, params)
+ # 等待一小段时间让状态更新
+ await asyncio.sleep(0.1)
+
+ # 生成结果信息
+ result = f"{device_name}的{method_name}操作执行成功"
+
+ # 根据方法名尝试自动更新状态
+ await update_state_by_method(conn, device_name, method_name, params)
+
+ # 处理响应中可能的占位符
+ response = response_success
+ # 替换{value}占位符
+ for param_name, param_value in params.items():
+ # 先尝试直接替换参数值
+ if "{" + param_name + "}" in response:
+ response = response.replace("{" + param_name + "}", str(param_value))
+
+ # 如果有{value}占位符,用相关参数替换
+ if "{value}" in response:
+ response = response.replace("{value}", str(param_value))
+ break
+
+ return ActionResponse(Action.RESPONSE, result, response)
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"执行{device_name}的{method_name}操作失败: {e}")
+
+ # 操作失败时使用大模型提供的失败响应
+ response = response_failure
+
+ return ActionResponse(Action.ERROR, str(e), response)
+
+ return wrap_async_function(iot_control_function)
+
+
+def create_iot_query_function(device_name, prop_name, prop_info):
+ """
+ 根据IOT设备属性创建查询函数
+ """
+
+ async def iot_query_function(conn, response_success=None, response_failure=None):
+ try:
+ # 打印响应参数
+ logger.bind(tag=TAG).info(
+ f"查询函数接收到的响应参数: success='{response_success}', failure='{response_failure}'")
+
+ value = await get_iot_status(conn, device_name, prop_name)
+
+ # 查询成功,生成结果
+ if value is not None:
+ # 使用大模型提供的成功响应,并替换其中的占位符
+ response = response_success.replace("{value}", str(value))
+
+ return ActionResponse(Action.RESPONSE, str(value), response)
+ else:
+ # 查询失败,使用大模型提供的失败响应
+ response = response_failure
+
+ return ActionResponse(Action.ERROR, f"属性{prop_name}不存在", response)
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"查询{device_name}的{prop_name}时出错: {e}")
+
+ # 查询出错时使用大模型提供的失败响应
+ response = response_failure
+
+ return ActionResponse(Action.ERROR, str(e), response)
+
+ return wrap_async_function(iot_query_function)
+
+
+async def update_state_by_method(conn, device_name, method_name, params):
+ """根据方法和参数自动更新设备状态"""
+ try:
+ # 规则1: 方法名为TurnOn,设置power为True
+ if method_name == "TurnOn":
+ await set_iot_status(conn, device_name, "power", True)
+
+ # 规则2: 方法名为TurnOff,设置power为False
+ elif method_name == "TurnOff":
+ await set_iot_status(conn, device_name, "power", False)
+
+ # 规则3: Set开头的方法,尝试更新对应参数
+ elif method_name.startswith("Set"):
+ # 从参数中找到可能的状态值
+ for param_name, param_value in params.items():
+ # 尝试更新对应名称的属性
+ await set_iot_status(conn, device_name, param_name, param_value)
+
+ # 其他方法,尝试直接从参数更新状态
+ else:
+ for param_name, param_value in params.items():
+ # 检查设备是否有此属性
+ status = await get_iot_status(conn, device_name, param_name)
+ if status is not None: # 属性存在
+ await set_iot_status(conn, device_name, param_name, param_value)
+ except Exception as e:
+ logger.bind(tag=TAG).warning(f"自动更新状态失败: {e}")
+
+
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):
@@ -29,17 +156,7 @@ class IotDescriptor:
# 根据描述创建属性
for key, value in properties.items():
- # "volume":{"description":"当前音量 值","type":"number"}
- """
- 等价于
- {
- 'name': 名字,
- 'description': 描述,
- 'value': 0
- }
- """
- # setattr(self, key, {}) # 创建一个空字典, 名字是属性名
- property_item = globals()[key] = {} # 创建一个空字典, 名字是属性名
+ property_item = globals()[key] = {}
property_item['name'] = key
property_item["description"] = value["description"]
if value["type"] == "number":
@@ -52,23 +169,10 @@ class IotDescriptor:
# 根据描述创建方法
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 = 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":
@@ -77,58 +181,136 @@ class IotDescriptor:
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"}}
+def register_device_type(descriptor):
+ """注册设备类型及其功能"""
+ device_name = descriptor["name"]
+ type_id = device_type_registry.generate_device_type_id(descriptor)
+
+ # 如果该类型已注册,直接返回类型ID
+ if type_id in device_type_registry.type_functions:
+ return type_id
+
+ functions = {}
+
+ # 为每个属性创建查询函数
+ for prop_name, prop_info in descriptor["properties"].items():
+ func_name = f"get_{device_name.lower()}_{prop_name.lower()}"
+ func_desc = {
+ "type": "function",
+ "function": {
+ "name": func_name,
+ "description": f"查询{descriptor['description']}的{prop_info['description']}",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "response_success": {
+ "type": "string",
+ "description": f"查询成功时的友好回复,必须使用{{value}}作为占位符表示查询到的值"
+ },
+ "response_failure": {
+ "type": "string",
+ "description": f"查询失败时的友好回复,例如:'无法获取{device_name}的{prop_info['description']}'"
+ }
+ },
+ "required": ["response_success", "response_failure"]
+ }
}
}
- }]
- descriptors: 描述列表
- """
+ query_func = create_iot_query_function(device_name, prop_name, prop_info)
+ decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(query_func)
+ functions[func_name] = decorated_func
+
+ # 为每个方法创建控制函数
+ for method_name, method_info in descriptor["methods"].items():
+ func_name = f"{device_name.lower()}_{method_name.lower()}"
+
+ # 创建参数字典,添加原有参数
+ parameters = {
+ param_name: {
+ "type": param_info["type"],
+ "description": param_info["description"]
+ }
+ for param_name, param_info in method_info["parameters"].items()
+ }
+
+ # 添加响应参数
+ parameters.update({
+ "response_success": {
+ "type": "string",
+ "description": "操作成功时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
+ },
+ "response_failure": {
+ "type": "string",
+ "description": "操作失败时的友好回复,关于该设备的操作结果,设备名称尽量使用description中的名称"
+ }
+ })
+
+ # 构建必须参数列表(原有参数 + 响应参数)
+ required_params = list(method_info["parameters"].keys())
+ required_params.extend(["response_success", "response_failure"])
+
+ func_desc = {
+ "type": "function",
+ "function": {
+ "name": func_name,
+ "description": f"{descriptor['description']} - {method_info['description']}",
+ "parameters": {
+ "type": "object",
+ "properties": parameters,
+ "required": required_params
+ }
+ }
+ }
+ control_func = create_iot_function(device_name, method_name, method_info)
+ decorated_func = register_function(func_name, func_desc, ToolType.IOT_CTL)(control_func)
+ functions[func_name] = decorated_func
+
+ device_type_registry.register_device_type(type_id, functions)
+ return type_id
+
+
+# 用于接受前端设备推送的搜索iot描述
+async def handleIotDescriptors(conn, descriptors):
+ """处理物联网描述"""
+ functions_changed = False
+
for descriptor in descriptors:
+ # 创建IOT设备描述符
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})
+ if conn.use_function_call_mode:
+ # 注册或获取设备类型
+ type_id = register_device_type(descriptor)
+ device_functions = device_type_registry.get_device_functions(type_id)
+
+ # 在连接级注册设备函数
+ if hasattr(conn, 'func_handler'):
+ for func_name in device_functions:
+ conn.func_handler.function_registry.register_function(func_name)
+ logger.bind(tag=TAG).info(f"注册IOT函数到function handler: {func_name}")
+ functions_changed = True
+
+ # 如果注册了新函数,更新function描述列表
+ if functions_changed and hasattr(conn, 'func_handler'):
+ conn.func_handler.upload_functions_desc()
+ func_names = conn.func_handler.current_support_functions()
+ logger.bind(tag=TAG).info(f"设备类型: {type_id}")
+ logger.bind(tag=TAG).info(f"更新function描述列表完成,当前支持的函数: {func_names}")
+
+
async def handleIotStatus(conn, states):
- """
- 处理物联网状态
- 示例: [{
- "name":"Speaker",
- "state":{
- "volume":100
- }
- }]
- states: 状态列表
- """
+ """处理物联网状态"""
for state in states:
for key, value in conn.iot_descriptors.items():
if key == state["name"]:
for property_item in value.properties:
- # properties为字典列表, 记录各种属性
for k, v in state["state"].items():
- # state为字典, 记录各种属性的值, 是需要记录的信息
if property_item["name"] == k:
- # 检查一下属性是不是相同的
if type(v) != type(property_item["value"]):
logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
break
@@ -138,41 +320,35 @@ async def handleIotStatus(conn, states):
break
break
+
async def get_iot_status(conn, name, property_name):
- """
- 获取物联网状态
- name: 设备名称 "Speaker"
- property_name: 属性名称 "volume"
- 返回值: 属性值, 实际的属性有int, bool和str三种类型
- """
+ """获取物联网状态"""
for key, value in conn.iot_descriptors.items():
if key == name:
for property_item in value.properties:
if property_item["name"] == property_name:
return property_item["value"]
+ logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
return None
-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
- }
- }
- ]
- }
- """
+async def set_iot_status(conn, name, property_name, value):
+ """设置物联网状态"""
+ for key, iot_descriptor in conn.iot_descriptors.items():
+ if key == name:
+ for property_item in iot_descriptor.properties:
+ if property_item["name"] == property_name:
+ if type(value) != type(property_item["value"]):
+ logger.bind(tag=TAG).error(f"属性{property_item['name']}的值类型不匹配")
+ return
+ property_item["value"] = value
+ logger.bind(tag=TAG).info(f"物联网状态更新: {name} , {property_name} = {value}")
+ return
+ logger.bind(tag=TAG).warning(f"未找到设备 {name} 的属性 {property_name}")
+
+
+async def send_iot_conn(conn, name, method_name, parameters):
+ """发送物联网指令"""
for key, value in conn.iot_descriptors.items():
if key == name:
# 找到了设备
diff --git a/main/xiaozhi-server/core/handle/textHandle.py b/main/xiaozhi-server/core/handle/textHandle.py
index cc28dbb4..514de127 100644
--- a/main/xiaozhi-server/core/handle/textHandle.py
+++ b/main/xiaozhi-server/core/handle/textHandle.py
@@ -2,7 +2,7 @@ from config.logger import setup_logging
import json
from core.handle.abortHandle import handleAbortMessage
from core.handle.helloHandle import handleHelloMessage
-from core.handle.receiveAudioHandle import startToChat
+from core.handle.receiveAudioHandle import startToChat, handleAudioMessage
from core.handle.iotHandle import handleIotDescriptors, handleIotStatus
TAG = __name__
@@ -24,13 +24,15 @@ async def handleTextMessage(conn, message):
elif msg_json["type"] == "listen":
if "mode" in msg_json:
conn.client_listen_mode = msg_json["mode"]
- logger.bind(tag=TAG).debug(f"客户端拾音模式:{conn.client_listen_mode}")
+ logger.bind(tag=TAG).debug(f"客户端拾音模式:{conn. client_listen_mode}")
if msg_json["state"] == "start":
conn.client_have_voice = True
conn.client_voice_stop = False
elif msg_json["state"] == "stop":
conn.client_have_voice = True
conn.client_voice_stop = True
+ if len(conn.asr_audio) > 0:
+ await handleAudioMessage(conn, b'')
elif msg_json["state"] == "detect":
conn.asr_server_receive = False
conn.client_have_voice = False
diff --git a/main/xiaozhi-server/core/providers/tts/base.py b/main/xiaozhi-server/core/providers/tts/base.py
index e6f013ca..d4a059f8 100644
--- a/main/xiaozhi-server/core/providers/tts/base.py
+++ b/main/xiaozhi-server/core/providers/tts/base.py
@@ -13,7 +13,7 @@ logger = setup_logging()
class TTSProviderBase(ABC):
def __init__(self, config, delete_audio_file):
self.delete_audio_file = delete_audio_file
- self.output_file = config.get("output_file")
+ self.output_file = config.get("output_dir")
@abstractmethod
def generate_filename(self):
diff --git a/main/xiaozhi-server/core/providers/tts/custom.py b/main/xiaozhi-server/core/providers/tts/custom.py
index d5447878..3417790f 100644
--- a/main/xiaozhi-server/core/providers/tts/custom.py
+++ b/main/xiaozhi-server/core/providers/tts/custom.py
@@ -15,7 +15,7 @@ class TTSProvider(TTSProviderBase):
self.headers = config.get("headers", {})
self.params = config.get("params")
self.format = config.get("format", "wav")
- self.output_file = config.get("output_file", "tmp/")
+ self.output_file = config.get("output_dir", "tmp/")
def generate_filename(self):
return os.path.join(self.output_file, f"tts-{datetime.now().date()}@{uuid.uuid4().hex}.{self.format}")
diff --git a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py
index befb14bc..ebfb12f1 100644
--- a/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py
+++ b/main/xiaozhi-server/core/providers/tts/gpt_sovits_v3.py
@@ -22,7 +22,7 @@ class TTSProvider(TTSProviderBase):
self.cut_punc = config.get("cut_punc","")
self.speed = config.get("speed", 1.0)
self.inp_refs = config.get("inp_refs",[])
- self.sample_steps = config.get("inp_refs",32)
+ self.sample_steps = config.get("sample_steps",32)
self.if_sr = config.get("if_sr",False)
diff --git a/main/xiaozhi-server/core/providers/tts/openai.py b/main/xiaozhi-server/core/providers/tts/openai.py
index dbdf311f..1849c128 100644
--- a/main/xiaozhi-server/core/providers/tts/openai.py
+++ b/main/xiaozhi-server/core/providers/tts/openai.py
@@ -14,7 +14,7 @@ class TTSProvider(TTSProviderBase):
self.voice = config.get("voice", "alloy")
self.response_format = "wav"
self.speed = config.get("speed", 1.0)
- self.output_file = config.get("output_file", "tmp/")
+ self.output_file = config.get("output_dir", "tmp/")
check_model_key("TTS", self.api_key)
def generate_filename(self, extension=".wav"):
diff --git a/main/xiaozhi-server/core/providers/tts/ttson.py b/main/xiaozhi-server/core/providers/tts/ttson.py
index e56fd02d..e9fee109 100644
--- a/main/xiaozhi-server/core/providers/tts/ttson.py
+++ b/main/xiaozhi-server/core/providers/tts/ttson.py
@@ -17,7 +17,7 @@ class TTSProvider(TTSProviderBase):
self.volume_change_dB = config.get("volume_change_dB", 0)
self.speed_factor = config.get("speed_factor", 1)
self.stream = config.get("stream", False)
- self.output_file = config.get("output_file")
+ self.output_file = config.get("output_dir")
self.pitch_factor = config.get("pitch_factor", 0)
self.format = config.get("format", "mp3")
self.emotion = config.get("emotion", 1)
diff --git a/main/xiaozhi-server/docker-compose.yml b/main/xiaozhi-server/docker-compose.yml
old mode 100755
new mode 100644
index d74d77f2..c22e7545
--- a/main/xiaozhi-server/docker-compose.yml
+++ b/main/xiaozhi-server/docker-compose.yml
@@ -21,6 +21,7 @@ services:
- ./data:/opt/xiaozhi-esp32-server/data
# 模型文件挂接,很重要
- ./models/SenseVoiceSmall/model.pt:/opt/xiaozhi-esp32-server/models/SenseVoiceSmall/model.pt
+
# #智控台还没开发好,还不能完全使用,会报很多错误,如果是非技术人员,请不要启用智控台服务
# xiaozhi-esp32-server-web:
# image: ghcr.nju.edu.cn/xinnan-tech/xiaozhi-esp32-server:web_latest
diff --git a/main/xiaozhi-server/plugins_func/functions/play_music.py b/main/xiaozhi-server/plugins_func/functions/play_music.py
index b9220ea5..75f3834d 100644
--- a/main/xiaozhi-server/plugins_func/functions/play_music.py
+++ b/main/xiaozhi-server/plugins_func/functions/play_music.py
@@ -112,7 +112,6 @@ def get_music_files(music_dir, music_ext):
def initialize_music_handler(conn):
global MUSIC_CACHE
if MUSIC_CACHE == {}:
- logger.bind(tag=TAG).info(f"实例化音乐:")
if "music" in conn.config:
MUSIC_CACHE["music_config"] = conn.config["music"]
MUSIC_CACHE["music_dir"] = os.path.abspath(
diff --git a/main/xiaozhi-server/plugins_func/functions/raise_and_lower_the_volume.py b/main/xiaozhi-server/plugins_func/functions/raise_and_lower_the_volume.py
new file mode 100644
index 00000000..c77cb78b
--- /dev/null
+++ b/main/xiaozhi-server/plugins_func/functions/raise_and_lower_the_volume.py
@@ -0,0 +1,64 @@
+from config.logger import setup_logging
+from plugins_func.register import register_function, ToolType, ActionResponse, Action
+from core.handle.iotHandle import get_iot_status, send_iot_conn
+import asyncio
+
+TAG = __name__
+logger = setup_logging()
+
+raise_and_lower_the_volume_function_desc = {
+ "type": "function",
+ "function": {
+ "name": "raise_and_lower_the_volume",
+ "description": "用户觉得声音过高或过低,或者用户想提高或降低音量。比如用户说太大声了,参数为:lower,比如用户说提高音量,参数为:raise",
+ "parameters": {
+ "type": "object",
+ "properties": {
+ "action": {
+ "type": "string",
+ "description": "动作名称,要么是raise,要么是lower"
+ }
+ },
+ "required": ["action"]
+ }
+ }
+}
+
+
+@register_function('raise_and_lower_the_volume', raise_and_lower_the_volume_function_desc, ToolType.IOT_CTL)
+def raise_and_lower_the_volume(conn, action: str):
+ """
+ 获取当前设备音量
+ """
+
+ future = asyncio.run_coroutine_threadsafe(
+ _raise_and_lower_the_volume(conn, action),
+ conn.loop
+ )
+
+ try:
+ new_volume = future.result() # 同步等待异步操作完成
+ logger.bind(tag=TAG).info(f"音量操作完成: {new_volume}")
+ response = f"音量已调整到{new_volume}"
+ except Exception as e:
+ logger.bind(tag=TAG).error(f"音量操作失败: {e}")
+ response = f"音量调整失败: {e}"
+
+ return ActionResponse(action=Action.RESPONSE, result="指令已接收", response=response)
+
+
+async def _raise_and_lower_the_volume(conn, action):
+ volume = await get_iot_status(conn, "Speaker", "volume")
+ if volume is None:
+ raise Exception("你的设备不支持音量控制")
+ if action == 'raise':
+ volume += 10
+ elif action == 'lower':
+ volume -= 10
+ # 限制音量范围在0到100之间
+ if volume < 0:
+ volume = 0
+ elif volume > 100:
+ volume = 100
+ await send_iot_conn(conn, "Speaker", "SetVolume", {"volume": volume})
+ return volume
diff --git a/main/xiaozhi-server/plugins_func/register.py b/main/xiaozhi-server/plugins_func/register.py
index 4c6db461..ccb03b44 100644
--- a/main/xiaozhi-server/plugins_func/register.py
+++ b/main/xiaozhi-server/plugins_func/register.py
@@ -11,6 +11,7 @@ class ToolType(Enum):
WAIT = (2, "调用工具,等待函数返回")
CHANGE_SYS_PROMPT = (3, "修改系统提示词,切换角色性格或职责")
SYSTEM_CTL = (4, "系统控制,影响正常的对话流程,如退出、播放音乐等,需要传递conn参数")
+ IOT_CTL = (5, "IOT设备控制,需要传递conn参数")
def __init__(self, code, message):
self.code = code
@@ -18,6 +19,7 @@ class ToolType(Enum):
class Action(Enum):
+ ERROR = (-1, "错误")
NOTFOUND = (0, "没有找到函数")
NONE = (1, "啥也不干")
RESPONSE = (2, "直接回复")
@@ -40,8 +42,31 @@ class FunctionItem:
self.func = func
self.type = type
+class DeviceTypeRegistry:
+ """设备类型注册表,用于管理IOT设备类型及其函数"""
+ def __init__(self):
+ self.type_functions = {} # type_signature -> {func_name: FunctionItem}
+
+ def generate_device_type_id(self, descriptor):
+ """通过设备能力描述生成类型ID"""
+ properties = sorted(descriptor["properties"].keys())
+ methods = sorted(descriptor["methods"].keys())
+ # 使用属性和方法的组合作为设备类型的唯一标识
+ type_signature = f"{descriptor['name']}:{','.join(properties)}:{','.join(methods)}"
+ return type_signature
+
+ def get_device_functions(self, type_id):
+ """获取设备类型对应的所有函数"""
+ return self.type_functions.get(type_id, {})
+
+ def register_device_type(self, type_id, functions):
+ """注册设备类型及其函数"""
+ if type_id not in self.type_functions:
+ self.type_functions[type_id] = functions
+
# 初始化函数注册字典
all_function_registry = {}
+device_type_registry = DeviceTypeRegistry()
def register_function(name, desc, type=None):
"""注册函数到函数注册字典的装饰器"""
diff --git a/main/xiaozhi-server/requirements.txt b/main/xiaozhi-server/requirements.txt
index bcd6b4ee..81d0cdfd 100755
--- a/main/xiaozhi-server/requirements.txt
+++ b/main/xiaozhi-server/requirements.txt
@@ -19,4 +19,4 @@ loguru==0.7.3
requests==2.32.3
cozepy==0.12.0
mem0ai==0.1.62
-bs4==0.0.2
\ No newline at end of file
+bs4==0.0.2