xiaozhi-sdk 0.0.6__tar.gz → 0.0.7__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of xiaozhi-sdk might be problematic. Click here for more details.
- {xiaozhi_sdk-0.0.6/xiaozhi_sdk.egg-info → xiaozhi_sdk-0.0.7}/PKG-INFO +10 -5
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/README.md +5 -1
- xiaozhi_sdk-0.0.7/file/audio/play_music.wav +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/pyproject.toml +4 -3
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/tests/test_xiaozhi.py +22 -11
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/__init__.py +1 -1
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/cli.py +2 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/core.py +16 -3
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/mcp.py +42 -5
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/opus.py +12 -6
- xiaozhi_sdk-0.0.6/xiaozhi_sdk/utils.py → xiaozhi_sdk-0.0.7/xiaozhi_sdk/utils/__init__.py +3 -3
- xiaozhi_sdk-0.0.6/xiaozhi_sdk/data.py → xiaozhi_sdk-0.0.7/xiaozhi_sdk/utils/mcp_data.py +16 -0
- xiaozhi_sdk-0.0.7/xiaozhi_sdk/utils/mcp_tool.py +88 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7/xiaozhi_sdk.egg-info}/PKG-INFO +10 -5
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk.egg-info/SOURCES.txt +7 -4
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk.egg-info/requires.txt +2 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/LICENSE +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/MANIFEST.in +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/audio/greet.wav +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/audio/say_hello.wav +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/audio/take_photo.wav +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/image/leijun.jpg +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/opus/linux-arm64-libopus.so +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/opus/linux-x64-libopus.so +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/opus/macos-arm64-libopus.dylib +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/file/opus/macos-x64-libopus.dylib +0 -0
- /xiaozhi_sdk-0.0.6/file/opus/windows-x86_64-opus.dll → /xiaozhi_sdk-0.0.7/file/opus/windows-opus.dll +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/setup.cfg +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/tests/test_iot.py +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/tests/test_pic.py +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/__main__.py +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/config.py +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk/iot.py +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk.egg-info/dependency_links.txt +0 -0
- {xiaozhi_sdk-0.0.6 → xiaozhi_sdk-0.0.7}/xiaozhi_sdk.egg-info/top_level.txt +0 -0
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xiaozhi-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
|
|
5
5
|
Author-email: dairoot <623815825@qq.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/dairoot/xiaozhi-sdk
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
9
|
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9
|
|
12
11
|
Description-Content-Type: text/markdown
|
|
13
12
|
License-File: LICENSE
|
|
14
13
|
Requires-Dist: numpy
|
|
@@ -21,6 +20,8 @@ Requires-Dist: sounddevice
|
|
|
21
20
|
Requires-Dist: python-socks
|
|
22
21
|
Requires-Dist: click
|
|
23
22
|
Requires-Dist: colorlog
|
|
23
|
+
Requires-Dist: soundfile>=0.13.1
|
|
24
|
+
Requires-Dist: pydub>=0.25.1
|
|
24
25
|
Dynamic: license-file
|
|
25
26
|
|
|
26
27
|
# 小智SDK (XiaoZhi SDK)
|
|
@@ -73,7 +74,11 @@ python -m xiaozhi_sdk 00:22:44:66:88:00
|
|
|
73
74
|
## ✅ 运行测试
|
|
74
75
|
|
|
75
76
|
```bash
|
|
76
|
-
|
|
77
|
+
# 安装开发依赖
|
|
78
|
+
uv sync --group dev
|
|
79
|
+
|
|
80
|
+
# 运行测试
|
|
81
|
+
uv run pytest
|
|
77
82
|
```
|
|
78
83
|
|
|
79
84
|
|
|
Binary file
|
|
@@ -8,8 +8,8 @@ dynamic = ["version"]
|
|
|
8
8
|
description = "一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
authors = [{name = "dairoot", email = "623815825@qq.com"}]
|
|
11
|
-
license =
|
|
12
|
-
requires-python = ">=3.
|
|
11
|
+
license = "MIT"
|
|
12
|
+
requires-python = ">=3.9"
|
|
13
13
|
dependencies = [
|
|
14
14
|
"numpy",
|
|
15
15
|
"websockets>=15.0.1",
|
|
@@ -21,10 +21,11 @@ dependencies = [
|
|
|
21
21
|
"python-socks",
|
|
22
22
|
"click",
|
|
23
23
|
"colorlog",
|
|
24
|
+
"soundfile>=0.13.1",
|
|
25
|
+
"pydub>=0.25.1",
|
|
24
26
|
]
|
|
25
27
|
classifiers = [
|
|
26
28
|
"Programming Language :: Python :: 3",
|
|
27
|
-
"License :: OSI Approved :: MIT License",
|
|
28
29
|
"Operating System :: OS Independent",
|
|
29
30
|
]
|
|
30
31
|
|
|
@@ -27,7 +27,11 @@ async def assistant_audio_play(audio_queue, wait_time=5):
|
|
|
27
27
|
continue
|
|
28
28
|
|
|
29
29
|
pcm_data = audio_queue.popleft()
|
|
30
|
-
|
|
30
|
+
|
|
31
|
+
# 将字节数据转换为 numpy int16 数组
|
|
32
|
+
audio_array = pcm_data
|
|
33
|
+
|
|
34
|
+
stream.write(audio_array)
|
|
31
35
|
last_time = time.time()
|
|
32
36
|
|
|
33
37
|
stream.stop()
|
|
@@ -59,10 +63,11 @@ def mcp_tool_func():
|
|
|
59
63
|
|
|
60
64
|
async def message_handler_callback(message):
|
|
61
65
|
print("message received:", message)
|
|
62
|
-
|
|
66
|
+
if message["type"] == "music":
|
|
67
|
+
print("music:", message["text"])
|
|
63
68
|
|
|
64
69
|
|
|
65
|
-
MAC_ADDR = "
|
|
70
|
+
MAC_ADDR = "00:22:44:66:88:00"
|
|
66
71
|
|
|
67
72
|
ota_url = "http://localhost:3080/api/ota"
|
|
68
73
|
URL = "ws://120.79.156.134:8380"
|
|
@@ -80,16 +85,22 @@ async def test_main():
|
|
|
80
85
|
await xiaozhi.set_mcp_tool_callback(mcp_tool_func())
|
|
81
86
|
await xiaozhi.init_connection(MAC_ADDR)
|
|
82
87
|
|
|
83
|
-
# say hellow
|
|
84
|
-
for pcm in read_audio_file("./file/audio/say_hello.wav"):
|
|
85
|
-
|
|
86
|
-
await xiaozhi.send_silence_audio()
|
|
87
|
-
await assistant_audio_play(xiaozhi.output_audio_queue)
|
|
88
|
+
# # say hellow
|
|
89
|
+
# for pcm in read_audio_file("./file/audio/say_hello.wav"):
|
|
90
|
+
# await xiaozhi.send_audio(pcm)
|
|
91
|
+
# await xiaozhi.send_silence_audio()
|
|
92
|
+
# await assistant_audio_play(xiaozhi.output_audio_queue)
|
|
93
|
+
|
|
94
|
+
# # say take photo
|
|
95
|
+
# for pcm in read_audio_file("./file/audio/take_photo.wav"):
|
|
96
|
+
# await xiaozhi.send_audio(pcm)
|
|
97
|
+
# await xiaozhi.send_silence_audio()
|
|
98
|
+
# await assistant_audio_play(xiaozhi.output_audio_queue, 5)
|
|
88
99
|
|
|
89
|
-
#
|
|
90
|
-
for pcm in read_audio_file("./file/audio/
|
|
100
|
+
# play music
|
|
101
|
+
for pcm in read_audio_file("./file/audio/play_music.wav"):
|
|
91
102
|
await xiaozhi.send_audio(pcm)
|
|
92
103
|
await xiaozhi.send_silence_audio()
|
|
93
|
-
await assistant_audio_play(xiaozhi.output_audio_queue,
|
|
104
|
+
await assistant_audio_play(xiaozhi.output_audio_queue, 500)
|
|
94
105
|
|
|
95
106
|
await xiaozhi.close()
|
|
@@ -87,9 +87,11 @@ class XiaoZhiClient:
|
|
|
87
87
|
"""启动客户端连接"""
|
|
88
88
|
self.mac_address = mac_address
|
|
89
89
|
self.xiaozhi = XiaoZhiWebsocket(handle_message, url=self.url, ota_url=self.ota_url, send_wake=True)
|
|
90
|
+
|
|
90
91
|
await self.xiaozhi.init_connection(
|
|
91
92
|
self.mac_address, aec=False, serial_number=serial_number, license_key=license_key
|
|
92
93
|
)
|
|
94
|
+
|
|
93
95
|
asyncio.create_task(play_assistant_audio(self.xiaozhi.output_audio_queue))
|
|
94
96
|
|
|
95
97
|
def audio_callback(self, indata, frames, time, status):
|
|
@@ -5,7 +5,7 @@ import os
|
|
|
5
5
|
import re
|
|
6
6
|
import uuid
|
|
7
7
|
from collections import deque
|
|
8
|
-
from typing import Any, Callable, Dict, Optional
|
|
8
|
+
from typing import Any, Callable, Deque, Dict, Optional
|
|
9
9
|
|
|
10
10
|
import websockets
|
|
11
11
|
|
|
@@ -13,6 +13,7 @@ from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
|
|
|
13
13
|
from xiaozhi_sdk.iot import OtaDevice
|
|
14
14
|
from xiaozhi_sdk.mcp import McpTool
|
|
15
15
|
from xiaozhi_sdk.utils import get_wav_info, read_audio_file, setup_opus
|
|
16
|
+
from xiaozhi_sdk.utils.mcp_tool import async_mcp_play_music, async_search_custom_music
|
|
16
17
|
|
|
17
18
|
setup_opus()
|
|
18
19
|
from xiaozhi_sdk.opus import AudioOpus
|
|
@@ -54,12 +55,17 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
54
55
|
self.message_handler_task: Optional[asyncio.Task] = None
|
|
55
56
|
|
|
56
57
|
# 输出音频
|
|
57
|
-
self.output_audio_queue:
|
|
58
|
+
self.output_audio_queue: Deque[bytes] = deque()
|
|
59
|
+
self.is_playing: bool = False
|
|
58
60
|
|
|
59
61
|
# OTA设备
|
|
60
62
|
self.ota: Optional[OtaDevice] = None
|
|
61
63
|
self.iot_task: Optional[asyncio.Task] = None
|
|
62
64
|
self.wait_device_activated: bool = False
|
|
65
|
+
self.tool_func = {
|
|
66
|
+
"async_play_custom_music": async_mcp_play_music,
|
|
67
|
+
"async_search_custom_music": async_search_custom_music,
|
|
68
|
+
}
|
|
63
69
|
|
|
64
70
|
async def _send_hello(self, aec: bool) -> None:
|
|
65
71
|
"""发送hello消息"""
|
|
@@ -148,6 +154,13 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
148
154
|
elif message_type == "mcp":
|
|
149
155
|
await self.mcp(data)
|
|
150
156
|
elif self.message_handler_callback:
|
|
157
|
+
if data["type"] == "tts":
|
|
158
|
+
if data["state"] == "sentence_start":
|
|
159
|
+
self.is_playing = True
|
|
160
|
+
# self.output_audio_queue.clear()
|
|
161
|
+
else:
|
|
162
|
+
self.is_playing = False
|
|
163
|
+
|
|
151
164
|
await self.message_handler_callback(data)
|
|
152
165
|
|
|
153
166
|
async def _message_handler(self) -> None:
|
|
@@ -164,7 +177,7 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
164
177
|
|
|
165
178
|
async def set_mcp_tool_callback(self, tool_func: Dict[str, Callable[..., Any]]) -> None:
|
|
166
179
|
"""设置MCP工具回调函数"""
|
|
167
|
-
self.tool_func
|
|
180
|
+
self.tool_func.update(tool_func)
|
|
168
181
|
|
|
169
182
|
async def connect_websocket(self, websocket_token):
|
|
170
183
|
"""连接websocket"""
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
+
import asyncio
|
|
1
2
|
import json
|
|
2
3
|
import logging
|
|
3
4
|
|
|
5
|
+
import numpy as np
|
|
4
6
|
import requests
|
|
5
7
|
|
|
6
|
-
from xiaozhi_sdk.
|
|
8
|
+
from xiaozhi_sdk.utils.mcp_data import mcp_initialize_payload, mcp_tool_conf, mcp_tools_payload
|
|
9
|
+
from xiaozhi_sdk.utils.mcp_tool import _get_random_music_info
|
|
7
10
|
|
|
8
11
|
logger = logging.getLogger("xiaozhi_sdk")
|
|
9
12
|
|
|
@@ -16,6 +19,7 @@ class McpTool(object):
|
|
|
16
19
|
self.explain_token = ""
|
|
17
20
|
self.websocket = None
|
|
18
21
|
self.tool_func = {}
|
|
22
|
+
self.is_playing = False
|
|
19
23
|
|
|
20
24
|
def get_mcp_json(self, payload: dict):
|
|
21
25
|
return json.dumps({"session_id": self.session_id, "type": "mcp", "payload": payload})
|
|
@@ -45,14 +49,46 @@ class McpTool(object):
|
|
|
45
49
|
return res_json, True
|
|
46
50
|
return res_json, False
|
|
47
51
|
|
|
52
|
+
async def play_custom_music(self, tool_func, arguments):
|
|
53
|
+
pcm_array, is_error = await tool_func(arguments)
|
|
54
|
+
while True:
|
|
55
|
+
if not self.is_playing:
|
|
56
|
+
break
|
|
57
|
+
await asyncio.sleep(0.1)
|
|
58
|
+
pcm_array = await self.audio_opus.change_sample_rate(np.array(pcm_array))
|
|
59
|
+
self.output_audio_queue.extend(pcm_array)
|
|
60
|
+
|
|
48
61
|
async def mcp_tool_call(self, mcp_json: dict):
|
|
49
62
|
tool_name = mcp_json["params"]["name"]
|
|
50
63
|
tool_func = self.tool_func[tool_name]
|
|
64
|
+
arguments = mcp_json["params"]["arguments"]
|
|
51
65
|
try:
|
|
52
|
-
|
|
66
|
+
if tool_name == "async_play_custom_music":
|
|
67
|
+
|
|
68
|
+
# v1 返回 url
|
|
69
|
+
music_info = await _get_random_music_info(arguments["id_list"])
|
|
70
|
+
if not music_info.get("url"):
|
|
71
|
+
tool_res, is_error = {"message": "播放失败"}, True
|
|
72
|
+
else:
|
|
73
|
+
tool_res, is_error = {"message": "正在为你播放: {}".format(arguments["music_name"])}, False
|
|
74
|
+
data = {
|
|
75
|
+
"type": "music", "state": "start",
|
|
76
|
+
"url": music_info["url"],
|
|
77
|
+
"text": arguments["music_name"],
|
|
78
|
+
"source": "sdk.mcp_music_tool"
|
|
79
|
+
}
|
|
80
|
+
await self.message_handler_callback(data)
|
|
81
|
+
|
|
82
|
+
# v2 音频放到输出
|
|
83
|
+
# asyncio.create_task(self.play_custom_music(tool_func, arguments))
|
|
84
|
+
|
|
85
|
+
elif tool_name.startswith("async_"):
|
|
86
|
+
tool_res, is_error = await tool_func(arguments)
|
|
87
|
+
else:
|
|
88
|
+
tool_res, is_error = tool_func(arguments)
|
|
53
89
|
except Exception as e:
|
|
54
90
|
logger.error("[MCP] tool_func error: %s", e)
|
|
55
|
-
return
|
|
91
|
+
return self._build_response(mcp_json["id"], "工具调用失败", True)
|
|
56
92
|
|
|
57
93
|
if tool_name == "take_photo":
|
|
58
94
|
tool_res, is_error = await self.analyze_image(tool_res, mcp_json["params"]["arguments"]["question"])
|
|
@@ -84,8 +120,9 @@ class McpTool(object):
|
|
|
84
120
|
for name, func in self.tool_func.items():
|
|
85
121
|
if func:
|
|
86
122
|
tool_list.append(name)
|
|
87
|
-
|
|
88
|
-
|
|
123
|
+
target_name = name.removeprefix("async_")
|
|
124
|
+
mcp_tool_conf[target_name]["name"] = name
|
|
125
|
+
mcp_tools_payload["result"]["tools"].append(mcp_tool_conf[target_name])
|
|
89
126
|
await self.websocket.send(self.get_mcp_json(mcp_tools_payload))
|
|
90
127
|
logger.debug("[MCP] 加载成功,当前可用工具列表为:%s", tool_list)
|
|
91
128
|
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import math
|
|
2
|
+
|
|
1
3
|
import av
|
|
2
4
|
import numpy as np
|
|
3
5
|
import opuslib
|
|
@@ -29,11 +31,16 @@ class AudioOpus:
|
|
|
29
31
|
pcm_bytes = pcm_array.tobytes()
|
|
30
32
|
return self.opus_encoder.encode(pcm_bytes, 960)
|
|
31
33
|
|
|
32
|
-
|
|
34
|
+
@staticmethod
|
|
35
|
+
def to_n_960(samples) -> np.ndarray:
|
|
36
|
+
n = math.ceil(samples.shape[0] / 960)
|
|
37
|
+
arr_padded = np.pad(samples, (0, 960 * n - samples.shape[0]), mode="constant", constant_values=0)
|
|
38
|
+
return arr_padded.reshape(n, 960)
|
|
39
|
+
|
|
40
|
+
async def change_sample_rate(self, pcm_array) -> np.ndarray:
|
|
33
41
|
if self.sample_rate == INPUT_SERVER_AUDIO_SAMPLE_RATE:
|
|
34
|
-
return
|
|
42
|
+
return self.to_n_960(pcm_array)
|
|
35
43
|
|
|
36
|
-
c = int(self.sample_rate / INPUT_SERVER_AUDIO_SAMPLE_RATE)
|
|
37
44
|
frame = av.AudioFrame.from_ndarray(np.array(pcm_array).reshape(1, -1), format="s16", layout="mono")
|
|
38
45
|
frame.sample_rate = INPUT_SERVER_AUDIO_SAMPLE_RATE # Assuming input is 16kHz
|
|
39
46
|
resampled_frames = self.resampler.resample(frame)
|
|
@@ -45,10 +52,9 @@ class AudioOpus:
|
|
|
45
52
|
)
|
|
46
53
|
new_frame.sample_rate = self.sample_rate
|
|
47
54
|
new_samples = new_frame.to_ndarray().flatten()
|
|
48
|
-
|
|
49
|
-
return arr_padded.reshape(c, 960)
|
|
55
|
+
return self.to_n_960(new_samples)
|
|
50
56
|
|
|
51
|
-
async def opus_to_pcm(self, opus):
|
|
57
|
+
async def opus_to_pcm(self, opus) -> np.ndarray:
|
|
52
58
|
pcm_data = self.opus_decoder.decode(opus, 960)
|
|
53
59
|
pcm_array = np.frombuffer(pcm_data, dtype=np.int16)
|
|
54
60
|
samples = await self.change_sample_rate(pcm_array)
|
|
@@ -45,11 +45,11 @@ def setup_opus():
|
|
|
45
45
|
arch = "x64"
|
|
46
46
|
|
|
47
47
|
if system == "darwin": # macOS
|
|
48
|
-
return f"{current_dir}
|
|
48
|
+
return f"{current_dir}/../../file/opus/macos-{arch}-libopus.dylib"
|
|
49
49
|
elif system == "windows": # Windows
|
|
50
|
-
return f"{current_dir}
|
|
50
|
+
return f"{current_dir}/../../file/opus/windows-opus.dll"
|
|
51
51
|
elif system == "linux": # Linux
|
|
52
|
-
return f"{current_dir}
|
|
52
|
+
return f"{current_dir}/../../file/opus/linux-{arch}-libopus.so"
|
|
53
53
|
else:
|
|
54
54
|
# 默认情况,尝试系统查找
|
|
55
55
|
return ctypes.util.find_library(name)
|
|
@@ -11,6 +11,22 @@ mcp_initialize_payload: Dict[str, Any] = {
|
|
|
11
11
|
}
|
|
12
12
|
|
|
13
13
|
mcp_tool_conf: Dict[str, Dict[str, Any]] = {
|
|
14
|
+
"search_custom_music": {
|
|
15
|
+
"description": "Search music and get music IDs. Use this tool when the user asks to search or play music. This tool returns a list of music with their IDs, which are required for playing music. Args:\n `music_name`: The name of the music to search\n `author_name`: The name of the music author (optional)",
|
|
16
|
+
"inputSchema": {
|
|
17
|
+
"type": "object",
|
|
18
|
+
"properties": {"music_name": {"type": "string"}, "author_name": {"type": "string"}},
|
|
19
|
+
"required": ["music_name"],
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
"play_custom_music": {
|
|
23
|
+
"description": "Play music using music IDs. IMPORTANT: You must call `search_custom_music` first to get the music IDs before using this tool. Use this tool after getting music IDs from search results. Args:\n `id_list`: The id list of the music to play (obtained from search_custom_music results). The list must contain more than 2 music IDs, and the system will randomly select one to play.\n `music_name`: The name of the music (obtained from search_custom_music results)",
|
|
24
|
+
"inputSchema": {
|
|
25
|
+
"type": "object",
|
|
26
|
+
"properties": {"music_name": {"type": "string"}, "id_list": {"type": "array", "items": {"type": "string"}, "minItems": 3}},
|
|
27
|
+
"required": ["music_name", "id_list"],
|
|
28
|
+
},
|
|
29
|
+
},
|
|
14
30
|
"get_device_status": {
|
|
15
31
|
"description": "Provides the real-time information of the device, including the current status of the audio speaker, screen, battery, network, etc.\nUse this tool for: \n1. Answering questions about current condition (e.g. what is the current volume of the audio speaker?)\n2. As the first step to control the device (e.g. turn up / down the volume of the audio speaker, etc.)",
|
|
16
32
|
"inputSchema": {"type": "object", "properties": {}},
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import io
|
|
2
|
+
import random
|
|
3
|
+
|
|
4
|
+
import aiohttp
|
|
5
|
+
import numpy as np
|
|
6
|
+
from pydub import AudioSegment
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
async def async_search_custom_music(data) -> tuple[dict, bool]:
|
|
10
|
+
search_url = f"https://music-api.gdstudio.xyz/api.php?types=search&name={data['music_name']}&count=100&pages=1"
|
|
11
|
+
|
|
12
|
+
# 为搜索请求设置 10 秒超时
|
|
13
|
+
timeout = aiohttp.ClientTimeout(total=10)
|
|
14
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
15
|
+
async with session.get(search_url) as response:
|
|
16
|
+
response_json = await response.json()
|
|
17
|
+
|
|
18
|
+
music_list = []
|
|
19
|
+
first_music_list = []
|
|
20
|
+
other_music_list1 = []
|
|
21
|
+
other_music_list2 = []
|
|
22
|
+
for line in response_json:
|
|
23
|
+
if data.get("author_name") and data["author_name"] in line["artist"][0]:
|
|
24
|
+
first_music_list.append(line)
|
|
25
|
+
elif data.get("author_name") and (data["author_name"] in line["artist"] or data["author_name"] in line["name"]):
|
|
26
|
+
other_music_list1.append(line)
|
|
27
|
+
else:
|
|
28
|
+
other_music_list2.append(line)
|
|
29
|
+
|
|
30
|
+
if len(first_music_list) <= 10:
|
|
31
|
+
music_list = first_music_list
|
|
32
|
+
random.shuffle(other_music_list2)
|
|
33
|
+
music_list = music_list + other_music_list1[: 20 - len(music_list)]
|
|
34
|
+
music_list = music_list + other_music_list2[: 20 - len(music_list)]
|
|
35
|
+
|
|
36
|
+
# print(data)
|
|
37
|
+
# print("找到音乐,数量:", len(first_music_list), len(music_list))
|
|
38
|
+
|
|
39
|
+
if not music_list:
|
|
40
|
+
return {}, False
|
|
41
|
+
return {"message": "已找到歌曲", "music_list": music_list}, False
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
async def _get_random_music_info(id_list: list) -> dict:
|
|
45
|
+
timeout = aiohttp.ClientTimeout(total=10)
|
|
46
|
+
async with aiohttp.ClientSession(timeout=timeout) as session:
|
|
47
|
+
random.shuffle(id_list)
|
|
48
|
+
|
|
49
|
+
for music_id in id_list:
|
|
50
|
+
url = f"https://music-api.gdstudio.xyz/api.php?types=url&id={music_id}&br=128"
|
|
51
|
+
async with session.get(url) as response:
|
|
52
|
+
res_json = await response.json()
|
|
53
|
+
if res_json.get("url"):
|
|
54
|
+
break
|
|
55
|
+
|
|
56
|
+
return res_json
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
async def async_mcp_play_music(data) -> tuple[list, bool]:
|
|
60
|
+
id_list = data["id_list"]
|
|
61
|
+
res_json = await _get_random_music_info(id_list)
|
|
62
|
+
|
|
63
|
+
if not res_json:
|
|
64
|
+
return [], False
|
|
65
|
+
|
|
66
|
+
pcm_list = []
|
|
67
|
+
buffer = io.BytesIO()
|
|
68
|
+
# 为下载音乐文件设置 60 秒超时(音乐文件可能比较大)
|
|
69
|
+
download_timeout = aiohttp.ClientTimeout(total=60)
|
|
70
|
+
async with aiohttp.ClientSession(timeout=download_timeout) as session:
|
|
71
|
+
async with session.get(res_json["url"]) as resp:
|
|
72
|
+
async for chunk in resp.content.iter_chunked(1024):
|
|
73
|
+
buffer.write(chunk)
|
|
74
|
+
|
|
75
|
+
buffer.seek(0)
|
|
76
|
+
audio = AudioSegment.from_mp3(buffer)
|
|
77
|
+
audio = audio.set_frame_rate(16000).set_channels(1).set_sample_width(2) # 2 bytes = 16 bits
|
|
78
|
+
pcm_data = audio.raw_data
|
|
79
|
+
|
|
80
|
+
chunk_size = 960 * 2
|
|
81
|
+
for i in range(0, len(pcm_data), chunk_size):
|
|
82
|
+
chunk = pcm_data[i: i + chunk_size]
|
|
83
|
+
|
|
84
|
+
if chunk: # 确保不添加空块
|
|
85
|
+
chunk = np.frombuffer(chunk, dtype=np.int16)
|
|
86
|
+
pcm_list.extend(chunk)
|
|
87
|
+
|
|
88
|
+
return pcm_list, False
|
|
@@ -1,14 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xiaozhi-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.7
|
|
4
4
|
Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
|
|
5
5
|
Author-email: dairoot <623815825@qq.com>
|
|
6
|
-
License: MIT
|
|
6
|
+
License-Expression: MIT
|
|
7
7
|
Project-URL: Homepage, https://github.com/dairoot/xiaozhi-sdk
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
10
9
|
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.9
|
|
12
11
|
Description-Content-Type: text/markdown
|
|
13
12
|
License-File: LICENSE
|
|
14
13
|
Requires-Dist: numpy
|
|
@@ -21,6 +20,8 @@ Requires-Dist: sounddevice
|
|
|
21
20
|
Requires-Dist: python-socks
|
|
22
21
|
Requires-Dist: click
|
|
23
22
|
Requires-Dist: colorlog
|
|
23
|
+
Requires-Dist: soundfile>=0.13.1
|
|
24
|
+
Requires-Dist: pydub>=0.25.1
|
|
24
25
|
Dynamic: license-file
|
|
25
26
|
|
|
26
27
|
# 小智SDK (XiaoZhi SDK)
|
|
@@ -73,7 +74,11 @@ python -m xiaozhi_sdk 00:22:44:66:88:00
|
|
|
73
74
|
## ✅ 运行测试
|
|
74
75
|
|
|
75
76
|
```bash
|
|
76
|
-
|
|
77
|
+
# 安装开发依赖
|
|
78
|
+
uv sync --group dev
|
|
79
|
+
|
|
80
|
+
# 运行测试
|
|
81
|
+
uv run pytest
|
|
77
82
|
```
|
|
78
83
|
|
|
79
84
|
|
|
@@ -3,6 +3,7 @@ MANIFEST.in
|
|
|
3
3
|
README.md
|
|
4
4
|
pyproject.toml
|
|
5
5
|
file/audio/greet.wav
|
|
6
|
+
file/audio/play_music.wav
|
|
6
7
|
file/audio/say_hello.wav
|
|
7
8
|
file/audio/take_photo.wav
|
|
8
9
|
file/image/leijun.jpg
|
|
@@ -10,7 +11,7 @@ file/opus/linux-arm64-libopus.so
|
|
|
10
11
|
file/opus/linux-x64-libopus.so
|
|
11
12
|
file/opus/macos-arm64-libopus.dylib
|
|
12
13
|
file/opus/macos-x64-libopus.dylib
|
|
13
|
-
file/opus/windows-
|
|
14
|
+
file/opus/windows-opus.dll
|
|
14
15
|
tests/test_iot.py
|
|
15
16
|
tests/test_pic.py
|
|
16
17
|
tests/test_xiaozhi.py
|
|
@@ -19,17 +20,16 @@ xiaozhi_sdk/__main__.py
|
|
|
19
20
|
xiaozhi_sdk/cli.py
|
|
20
21
|
xiaozhi_sdk/config.py
|
|
21
22
|
xiaozhi_sdk/core.py
|
|
22
|
-
xiaozhi_sdk/data.py
|
|
23
23
|
xiaozhi_sdk/iot.py
|
|
24
24
|
xiaozhi_sdk/mcp.py
|
|
25
25
|
xiaozhi_sdk/opus.py
|
|
26
|
-
xiaozhi_sdk/utils.py
|
|
27
26
|
xiaozhi_sdk.egg-info/PKG-INFO
|
|
28
27
|
xiaozhi_sdk.egg-info/SOURCES.txt
|
|
29
28
|
xiaozhi_sdk.egg-info/dependency_links.txt
|
|
30
29
|
xiaozhi_sdk.egg-info/requires.txt
|
|
31
30
|
xiaozhi_sdk.egg-info/top_level.txt
|
|
32
31
|
xiaozhi_sdk/../file/audio/greet.wav
|
|
32
|
+
xiaozhi_sdk/../file/audio/play_music.wav
|
|
33
33
|
xiaozhi_sdk/../file/audio/say_hello.wav
|
|
34
34
|
xiaozhi_sdk/../file/audio/take_photo.wav
|
|
35
35
|
xiaozhi_sdk/../file/image/leijun.jpg
|
|
@@ -37,4 +37,7 @@ xiaozhi_sdk/../file/opus/linux-arm64-libopus.so
|
|
|
37
37
|
xiaozhi_sdk/../file/opus/linux-x64-libopus.so
|
|
38
38
|
xiaozhi_sdk/../file/opus/macos-arm64-libopus.dylib
|
|
39
39
|
xiaozhi_sdk/../file/opus/macos-x64-libopus.dylib
|
|
40
|
-
xiaozhi_sdk/../file/opus/windows-
|
|
40
|
+
xiaozhi_sdk/../file/opus/windows-opus.dll
|
|
41
|
+
xiaozhi_sdk/utils/__init__.py
|
|
42
|
+
xiaozhi_sdk/utils/mcp_data.py
|
|
43
|
+
xiaozhi_sdk/utils/mcp_tool.py
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
/xiaozhi_sdk-0.0.6/file/opus/windows-x86_64-opus.dll → /xiaozhi_sdk-0.0.7/file/opus/windows-opus.dll
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|