xiaozhi-sdk 0.0.2__tar.gz → 0.0.4__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.2/xiaozhi_sdk.egg-info → xiaozhi_sdk-0.0.4}/PKG-INFO +11 -17
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/README.md +6 -3
- xiaozhi_sdk-0.0.4/pyproject.toml +64 -0
- xiaozhi_sdk-0.0.4/setup.cfg +4 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/tests/test_iot.py +6 -2
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/__init__.py +39 -17
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/__main__.py +33 -11
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/data.py +1 -1
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/iot.py +8 -9
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/mcp.py +8 -6
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4/xiaozhi_sdk.egg-info}/PKG-INFO +11 -17
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk.egg-info/SOURCES.txt +1 -2
- xiaozhi_sdk-0.0.2/setup.cfg +0 -17
- xiaozhi_sdk-0.0.2/setup.py +0 -49
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/LICENSE +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/MANIFEST.in +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/audio/greet.wav +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/audio/say_hello.wav +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/audio/take_photo.wav +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/image/leijun.jpg +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/opus/linux-arm64-libopus.so +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/opus/linux-x64-libopus.so +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/opus/macos-arm64-libopus.dylib +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/opus/macos-x64-libopus.dylib +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/file/opus/windows-x86_64-opus.dll +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/tests/test_opus.py +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/tests/test_pic.py +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/tests/test_xiaozhi.py +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/config.py +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/opus.py +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk/utils.py +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk.egg-info/dependency_links.txt +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk.egg-info/requires.txt +0 -0
- {xiaozhi_sdk-0.0.2 → xiaozhi_sdk-0.0.4}/xiaozhi_sdk.egg-info/top_level.txt +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xiaozhi-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Author-email: dairoot <623815825@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dairoot/xiaozhi-sdk
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.8
|
|
11
|
+
Requires-Python: >=3.8.1
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
@@ -19,16 +19,7 @@ Requires-Dist: opuslib
|
|
|
19
19
|
Requires-Dist: requests
|
|
20
20
|
Requires-Dist: sounddevice
|
|
21
21
|
Requires-Dist: python-socks
|
|
22
|
-
Dynamic: author
|
|
23
|
-
Dynamic: author-email
|
|
24
|
-
Dynamic: classifier
|
|
25
|
-
Dynamic: description
|
|
26
|
-
Dynamic: description-content-type
|
|
27
|
-
Dynamic: home-page
|
|
28
22
|
Dynamic: license-file
|
|
29
|
-
Dynamic: requires-dist
|
|
30
|
-
Dynamic: requires-python
|
|
31
|
-
Dynamic: summary
|
|
32
23
|
|
|
33
24
|
# 小智SDK (XiaoZhi SDK)
|
|
34
25
|
|
|
@@ -71,9 +62,12 @@ positional arguments:
|
|
|
71
62
|
device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
72
63
|
|
|
73
64
|
options:
|
|
74
|
-
-h, --help
|
|
75
|
-
--url URL
|
|
76
|
-
--ota_url OTA_URL
|
|
65
|
+
-h, --help show this help message and exit
|
|
66
|
+
--url URL 服务端websocket地址
|
|
67
|
+
--ota_url OTA_URL OTA地址
|
|
68
|
+
--serial_number SERIAL_NUMBER 设备的序列号
|
|
69
|
+
--license_key LICENSE_KEY 设备的授权密钥
|
|
70
|
+
|
|
77
71
|
```
|
|
78
72
|
|
|
79
73
|
#### 连接设备(需要提供 MAC 地址)
|
|
@@ -39,9 +39,12 @@ positional arguments:
|
|
|
39
39
|
device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
40
40
|
|
|
41
41
|
options:
|
|
42
|
-
-h, --help
|
|
43
|
-
--url URL
|
|
44
|
-
--ota_url OTA_URL
|
|
42
|
+
-h, --help show this help message and exit
|
|
43
|
+
--url URL 服务端websocket地址
|
|
44
|
+
--ota_url OTA_URL OTA地址
|
|
45
|
+
--serial_number SERIAL_NUMBER 设备的序列号
|
|
46
|
+
--license_key LICENSE_KEY 设备的授权密钥
|
|
47
|
+
|
|
45
48
|
```
|
|
46
49
|
|
|
47
50
|
#### 连接设备(需要提供 MAC 地址)
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "xiaozhi-sdk"
|
|
7
|
+
dynamic = ["version"]
|
|
8
|
+
description = "一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
authors = [{name = "dairoot", email = "623815825@qq.com"}]
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
requires-python = ">=3.8.1"
|
|
13
|
+
dependencies = [
|
|
14
|
+
"numpy",
|
|
15
|
+
"websockets",
|
|
16
|
+
"aiohttp",
|
|
17
|
+
"av",
|
|
18
|
+
"opuslib",
|
|
19
|
+
"requests",
|
|
20
|
+
"sounddevice",
|
|
21
|
+
"python-socks",
|
|
22
|
+
]
|
|
23
|
+
classifiers = [
|
|
24
|
+
"Programming Language :: Python :: 3",
|
|
25
|
+
"License :: OSI Approved :: MIT License",
|
|
26
|
+
"Operating System :: OS Independent",
|
|
27
|
+
]
|
|
28
|
+
|
|
29
|
+
[project.urls]
|
|
30
|
+
Homepage = "https://github.com/dairoot/xiaozhi-sdk"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
[tool.setuptools.dynamic]
|
|
35
|
+
version = {attr = "xiaozhi_sdk.__version__"}
|
|
36
|
+
|
|
37
|
+
[tool.setuptools.packages.find]
|
|
38
|
+
include = ["xiaozhi_sdk*"]
|
|
39
|
+
|
|
40
|
+
[tool.setuptools.package-data]
|
|
41
|
+
xiaozhi_sdk = ["../file/**/*"]
|
|
42
|
+
|
|
43
|
+
# 保留现有的工具配置
|
|
44
|
+
[tool.coverage.run]
|
|
45
|
+
omit = [
|
|
46
|
+
"xiaozhi_sdk/__main__.py",
|
|
47
|
+
"tests/*",
|
|
48
|
+
]
|
|
49
|
+
|
|
50
|
+
[dependency-groups]
|
|
51
|
+
dev = [
|
|
52
|
+
"black>=24.8.0",
|
|
53
|
+
"flake8>=5.0.4",
|
|
54
|
+
"flake8-bugbear",
|
|
55
|
+
"flake8-comprehensions",
|
|
56
|
+
"isort>=5.13.2",
|
|
57
|
+
"mypy>=1.14.1",
|
|
58
|
+
"types-requests",
|
|
59
|
+
"pre-commit>=3.5.0",
|
|
60
|
+
"pytest>=8.3.5",
|
|
61
|
+
"pytest-asyncio",
|
|
62
|
+
"pytest-cov",
|
|
63
|
+
"build>=0.10",
|
|
64
|
+
]
|
|
@@ -13,7 +13,11 @@ from xiaozhi_sdk.iot import OtaDevice
|
|
|
13
13
|
async def test_main():
|
|
14
14
|
serial_number = ""
|
|
15
15
|
license_key = ""
|
|
16
|
-
mac_address = "00:22:44:66:88:
|
|
17
|
-
|
|
16
|
+
mac_address = "00:22:44:66:88:04"
|
|
17
|
+
ota_url = "http://localhost:3080/api/ota"
|
|
18
|
+
|
|
19
|
+
ota = OtaDevice(mac_addr=mac_address, client_id=str(uuid.uuid4()), serial_number=serial_number, ota_url=ota_url)
|
|
18
20
|
res = await ota.activate_device()
|
|
21
|
+
if not res.get("activation"):
|
|
22
|
+
return
|
|
19
23
|
await ota.check_activate(res["activation"]["challenge"], license_key)
|
|
@@ -1,5 +1,8 @@
|
|
|
1
|
+
__version__ = "0.0.4"
|
|
2
|
+
|
|
1
3
|
import asyncio
|
|
2
4
|
import json
|
|
5
|
+
import logging
|
|
3
6
|
import os
|
|
4
7
|
import re
|
|
5
8
|
import uuid
|
|
@@ -16,6 +19,8 @@ from xiaozhi_sdk.utils import get_wav_info, read_audio_file, setup_opus
|
|
|
16
19
|
setup_opus()
|
|
17
20
|
from xiaozhi_sdk.opus import AudioOpus
|
|
18
21
|
|
|
22
|
+
logger = logging.getLogger("xiaozhi_sdk")
|
|
23
|
+
|
|
19
24
|
|
|
20
25
|
class XiaoZhiWebsocket(McpTool):
|
|
21
26
|
|
|
@@ -75,7 +80,7 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
75
80
|
listen_message = {"session_id": self.session_id, "type": "listen", "state": "start", "mode": "realtime"}
|
|
76
81
|
await self.websocket.send(json.dumps(listen_message))
|
|
77
82
|
|
|
78
|
-
async def _activate_iot_device(self, ota_info: Dict[str, Any]) -> None:
|
|
83
|
+
async def _activate_iot_device(self, license_key: str, ota_info: Dict[str, Any]) -> None:
|
|
79
84
|
"""激活IoT设备"""
|
|
80
85
|
if not ota_info.get("activation"):
|
|
81
86
|
return
|
|
@@ -88,7 +93,7 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
88
93
|
await asyncio.sleep(3)
|
|
89
94
|
|
|
90
95
|
for _ in range(10):
|
|
91
|
-
if await self.ota.check_activate(challenge):
|
|
96
|
+
if await self.ota.check_activate(challenge, license_key):
|
|
92
97
|
break
|
|
93
98
|
await asyncio.sleep(3)
|
|
94
99
|
|
|
@@ -114,20 +119,23 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
114
119
|
|
|
115
120
|
async def _handle_websocket_message(self, message: Any) -> None:
|
|
116
121
|
"""处理接受到的WebSocket消息"""
|
|
122
|
+
|
|
123
|
+
# audio data
|
|
117
124
|
if isinstance(message, bytes):
|
|
118
125
|
pcm_array = await self.audio_opus.opus_to_pcm(message)
|
|
119
126
|
self.output_audio_queue.extend(pcm_array)
|
|
120
|
-
|
|
121
|
-
data = json.loads(message)
|
|
122
|
-
message_type = data["type"]
|
|
127
|
+
return
|
|
123
128
|
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
129
|
+
# json message
|
|
130
|
+
data = json.loads(message)
|
|
131
|
+
message_type = data["type"]
|
|
132
|
+
if message_type == "hello":
|
|
133
|
+
self.hello_received.set()
|
|
134
|
+
self.session_id = data["session_id"]
|
|
135
|
+
elif message_type == "mcp":
|
|
136
|
+
await self.mcp(data)
|
|
137
|
+
elif self.message_handler_callback:
|
|
138
|
+
await self.message_handler_callback(data)
|
|
131
139
|
|
|
132
140
|
async def _message_handler(self) -> None:
|
|
133
141
|
"""消息处理器"""
|
|
@@ -139,12 +147,15 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
139
147
|
await self.message_handler_callback(
|
|
140
148
|
{"type": "websocket", "state": "close", "source": "sdk.message_handler"}
|
|
141
149
|
)
|
|
150
|
+
logger.info("[websocket] close")
|
|
142
151
|
|
|
143
152
|
async def set_mcp_tool_callback(self, tool_func: Dict[str, Callable[..., Any]]) -> None:
|
|
144
153
|
"""设置MCP工具回调函数"""
|
|
145
154
|
self.tool_func = tool_func
|
|
146
155
|
|
|
147
|
-
async def init_connection(
|
|
156
|
+
async def init_connection(
|
|
157
|
+
self, mac_addr: str, aec: bool = False, serial_number: str = "", license_key: str = ""
|
|
158
|
+
) -> None:
|
|
148
159
|
"""初始化连接"""
|
|
149
160
|
# 校验MAC地址格式 XX:XX:XX:XX:XX:XX
|
|
150
161
|
mac_pattern = r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
|
|
@@ -152,8 +163,14 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
152
163
|
raise ValueError(f"无效的MAC地址格式: {mac_addr}。正确格式应为 XX:XX:XX:XX:XX:XX")
|
|
153
164
|
|
|
154
165
|
self.mac_addr = mac_addr.lower()
|
|
155
|
-
|
|
166
|
+
|
|
167
|
+
self.ota = OtaDevice(self.mac_addr, self.client_id, self.ota_url, serial_number)
|
|
156
168
|
ota_info = await self.ota.activate_device()
|
|
169
|
+
ws_url = ota_info["websocket"]["url"]
|
|
170
|
+
self.url = self.url or ws_url
|
|
171
|
+
|
|
172
|
+
if "tenclass.net" not in self.url and "xiaozhi.me" not in self.url:
|
|
173
|
+
logger.warning("[websocket] 检测到非官方服务器,请谨慎使用!当前链接地址: %s", self.url)
|
|
157
174
|
|
|
158
175
|
headers = {
|
|
159
176
|
"Authorization": "Bearer {}".format(ota_info["websocket"]["token"]),
|
|
@@ -161,13 +178,16 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
161
178
|
"Device-Id": self.mac_addr,
|
|
162
179
|
"Client-Id": self.client_id,
|
|
163
180
|
}
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
try:
|
|
182
|
+
self.websocket = await websockets.connect(uri=self.url, additional_headers=headers)
|
|
183
|
+
except websockets.exceptions.InvalidMessage as e:
|
|
184
|
+
logger.error("[websocket] 连接失败,请检查网络连接或设备状态。当前链接地址: %s, 错误信息:%s", self.url, e)
|
|
185
|
+
return
|
|
166
186
|
self.message_handler_task = asyncio.create_task(self._message_handler())
|
|
167
187
|
|
|
168
188
|
await self._send_hello(aec)
|
|
169
189
|
await self._start_listen()
|
|
170
|
-
asyncio.create_task(self._activate_iot_device(ota_info))
|
|
190
|
+
asyncio.create_task(self._activate_iot_device(license_key, ota_info))
|
|
171
191
|
await asyncio.sleep(0.5)
|
|
172
192
|
|
|
173
193
|
async def send_audio(self, pcm: bytes) -> None:
|
|
@@ -183,6 +203,8 @@ class XiaoZhiWebsocket(McpTool):
|
|
|
183
203
|
if self.message_handler_callback:
|
|
184
204
|
await self.message_handler_callback({"type": "websocket", "state": "close", "source": "sdk.send_audio"})
|
|
185
205
|
self.websocket = None
|
|
206
|
+
logger.info("[websocket] close")
|
|
207
|
+
|
|
186
208
|
await asyncio.sleep(0.5)
|
|
187
209
|
else:
|
|
188
210
|
await asyncio.sleep(0.1)
|
|
@@ -20,11 +20,15 @@ logger = logging.getLogger("xiaozhi_sdk")
|
|
|
20
20
|
# 全局状态
|
|
21
21
|
input_audio_buffer: deque[bytes] = deque()
|
|
22
22
|
is_playing_audio = False
|
|
23
|
+
is_end = False
|
|
23
24
|
|
|
24
25
|
|
|
25
26
|
async def handle_message(message):
|
|
26
27
|
"""处理接收到的消息"""
|
|
28
|
+
global is_end
|
|
27
29
|
logger.info("message received: %s", message)
|
|
30
|
+
if message["type"] == "websocket" and message["state"] == "close":
|
|
31
|
+
is_end = True
|
|
28
32
|
|
|
29
33
|
|
|
30
34
|
async def play_assistant_audio(audio_queue: deque[bytes]):
|
|
@@ -36,6 +40,9 @@ async def play_assistant_audio(audio_queue: deque[bytes]):
|
|
|
36
40
|
last_audio_time = None
|
|
37
41
|
|
|
38
42
|
while True:
|
|
43
|
+
if is_end:
|
|
44
|
+
return
|
|
45
|
+
|
|
39
46
|
if not audio_queue:
|
|
40
47
|
await asyncio.sleep(0.01)
|
|
41
48
|
if last_audio_time and time.time() - last_audio_time > 1:
|
|
@@ -51,16 +58,22 @@ async def play_assistant_audio(audio_queue: deque[bytes]):
|
|
|
51
58
|
class XiaoZhiClient:
|
|
52
59
|
"""小智客户端类"""
|
|
53
60
|
|
|
54
|
-
def __init__(
|
|
55
|
-
self
|
|
61
|
+
def __init__(
|
|
62
|
+
self,
|
|
63
|
+
url: Optional[str] = None,
|
|
64
|
+
ota_url: Optional[str] = None,
|
|
65
|
+
):
|
|
56
66
|
self.xiaozhi: Optional[XiaoZhiWebsocket] = None
|
|
57
67
|
self.url = url
|
|
58
68
|
self.ota_url = ota_url
|
|
59
69
|
|
|
60
|
-
async def start(self):
|
|
70
|
+
async def start(self, mac_address: str, serial_number: str = "", license_key: str = ""):
|
|
61
71
|
"""启动客户端连接"""
|
|
72
|
+
self.mac_address = mac_address
|
|
62
73
|
self.xiaozhi = XiaoZhiWebsocket(handle_message, url=self.url, ota_url=self.ota_url)
|
|
63
|
-
await self.xiaozhi.init_connection(
|
|
74
|
+
await self.xiaozhi.init_connection(
|
|
75
|
+
self.mac_address, aec=False, serial_number=serial_number, license_key=license_key
|
|
76
|
+
)
|
|
64
77
|
asyncio.create_task(play_assistant_audio(self.xiaozhi.output_audio_queue))
|
|
65
78
|
|
|
66
79
|
def audio_callback(self, indata, frames, time, status):
|
|
@@ -71,6 +84,10 @@ class XiaoZhiClient:
|
|
|
71
84
|
async def process_audio_input(self):
|
|
72
85
|
"""处理音频输入"""
|
|
73
86
|
while True:
|
|
87
|
+
|
|
88
|
+
if is_end:
|
|
89
|
+
return
|
|
90
|
+
|
|
74
91
|
if not input_audio_buffer:
|
|
75
92
|
await asyncio.sleep(0.02)
|
|
76
93
|
continue
|
|
@@ -83,19 +100,24 @@ class XiaoZhiClient:
|
|
|
83
100
|
async def main():
|
|
84
101
|
"""主函数"""
|
|
85
102
|
parser = argparse.ArgumentParser(description="小智SDK客户端")
|
|
86
|
-
parser.add_argument("device", help="
|
|
87
|
-
parser.add_argument("--url", help="
|
|
88
|
-
parser.add_argument("--ota_url", help="
|
|
103
|
+
parser.add_argument("device", help="设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)")
|
|
104
|
+
parser.add_argument("--url", help="服务端websocket地址")
|
|
105
|
+
parser.add_argument("--ota_url", help="OTA地址")
|
|
106
|
+
|
|
107
|
+
parser.add_argument("--serial_number", default="", help="设备的序列号")
|
|
108
|
+
parser.add_argument("--license_key", default="", help="设备的授权密钥")
|
|
89
109
|
|
|
90
110
|
args = parser.parse_args()
|
|
91
111
|
logger.info("Recording... Press Ctrl+C to stop.")
|
|
92
|
-
|
|
93
|
-
client
|
|
94
|
-
await client.start()
|
|
112
|
+
client = XiaoZhiClient(args.url, args.ota_url)
|
|
113
|
+
await client.start(args.device, args.serial_number, args.license_key)
|
|
95
114
|
|
|
96
115
|
with sd.InputStream(callback=client.audio_callback, channels=1, samplerate=16000, blocksize=960):
|
|
97
116
|
await client.process_audio_input()
|
|
98
117
|
|
|
99
118
|
|
|
100
119
|
if __name__ == "__main__":
|
|
101
|
-
|
|
120
|
+
try:
|
|
121
|
+
asyncio.run(main())
|
|
122
|
+
except KeyboardInterrupt:
|
|
123
|
+
logger.info("Stopping...")
|
|
@@ -16,7 +16,7 @@ mcp_tool_conf: Dict[str, Dict[str, Any]] = {
|
|
|
16
16
|
"inputSchema": {"type": "object", "properties": {}},
|
|
17
17
|
},
|
|
18
18
|
"set_volume": {
|
|
19
|
-
"description": "Set the volume of the audio speaker. If the current volume is unknown, you must call `
|
|
19
|
+
"description": "Set the volume of the audio speaker. If the current volume is unknown, you must call `get_device_status` tool first and then call this tool.",
|
|
20
20
|
"inputSchema": {
|
|
21
21
|
"type": "object",
|
|
22
22
|
"properties": {"volume": {"type": "integer", "minimum": 0, "maximum": 100}},
|
|
@@ -5,13 +5,13 @@ from typing import Any, Dict, Optional
|
|
|
5
5
|
|
|
6
6
|
import aiohttp
|
|
7
7
|
|
|
8
|
+
from xiaozhi_sdk import __version__
|
|
8
9
|
from xiaozhi_sdk.config import OTA_URL
|
|
9
10
|
|
|
10
11
|
# 常量定义
|
|
11
|
-
DEFAULT_APPLICATION_VERSION = "1.0.0"
|
|
12
12
|
BOARD_TYPE = "xiaozhi-sdk-box"
|
|
13
|
-
USER_AGENT = "xiaozhi-sdk/{}".format(
|
|
14
|
-
BOARD_NAME = "xiaozhi-sdk-{}".format(
|
|
13
|
+
USER_AGENT = "xiaozhi-sdk/{}".format(__version__)
|
|
14
|
+
BOARD_NAME = "xiaozhi-sdk-{}".format(__version__)
|
|
15
15
|
|
|
16
16
|
|
|
17
17
|
class OtaDevice:
|
|
@@ -29,6 +29,8 @@ class OtaDevice:
|
|
|
29
29
|
|
|
30
30
|
def __init__(self, mac_addr: str, client_id: str, ota_url: Optional[str] = None, serial_number: str = "") -> None:
|
|
31
31
|
self.ota_url = ota_url or OTA_URL
|
|
32
|
+
self.ota_url = self.ota_url.rstrip("/")
|
|
33
|
+
|
|
32
34
|
self.mac_addr = mac_addr
|
|
33
35
|
self.client_id = client_id
|
|
34
36
|
self.serial_number = serial_number
|
|
@@ -46,7 +48,7 @@ class OtaDevice:
|
|
|
46
48
|
headers["serial-number"] = self.serial_number
|
|
47
49
|
|
|
48
50
|
payload = {
|
|
49
|
-
"application": {"version":
|
|
51
|
+
"application": {"version": __version__},
|
|
50
52
|
"board": {
|
|
51
53
|
"type": BOARD_TYPE,
|
|
52
54
|
"name": BOARD_NAME,
|
|
@@ -60,16 +62,13 @@ class OtaDevice:
|
|
|
60
62
|
|
|
61
63
|
async def check_activate(self, challenge: str, license_key: str = "") -> bool:
|
|
62
64
|
url = f"{self.ota_url}/activate"
|
|
65
|
+
|
|
63
66
|
headers = self._get_base_headers()
|
|
64
67
|
|
|
65
68
|
hmac_instance = hmac.new(license_key.encode(), challenge.encode(), hashlib.sha256)
|
|
66
69
|
hmac_result = hmac_instance.hexdigest()
|
|
67
70
|
|
|
68
|
-
payload = {
|
|
69
|
-
"serial_number": self.serial_number,
|
|
70
|
-
"challenge": challenge,
|
|
71
|
-
"hmac": hmac_result
|
|
72
|
-
}
|
|
71
|
+
payload = {"serial_number": self.serial_number, "challenge": challenge, "hmac": hmac_result}
|
|
73
72
|
|
|
74
73
|
async with aiohttp.ClientSession() as session:
|
|
75
74
|
async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
|
|
@@ -51,7 +51,7 @@ class McpTool(object):
|
|
|
51
51
|
try:
|
|
52
52
|
tool_res, is_error = tool_func(mcp_json["params"]["arguments"])
|
|
53
53
|
except Exception as e:
|
|
54
|
-
logger.error("tool_func error: %s", e)
|
|
54
|
+
logger.error("[MCP] tool_func error: %s", e)
|
|
55
55
|
return
|
|
56
56
|
|
|
57
57
|
if tool_name == "take_photo":
|
|
@@ -76,7 +76,7 @@ class McpTool(object):
|
|
|
76
76
|
pass
|
|
77
77
|
|
|
78
78
|
elif method == "notifications/cancelled":
|
|
79
|
-
logger.error("MCP 工具加载失败")
|
|
79
|
+
logger.error("[MCP] 工具加载失败")
|
|
80
80
|
|
|
81
81
|
elif method == "tools/list":
|
|
82
82
|
mcp_tools_payload["id"] = payload["id"]
|
|
@@ -87,14 +87,16 @@ class McpTool(object):
|
|
|
87
87
|
mcp_tool_conf[name]["name"] = name
|
|
88
88
|
mcp_tools_payload["result"]["tools"].append(mcp_tool_conf[name])
|
|
89
89
|
await self.websocket.send(self.get_mcp_json(mcp_tools_payload))
|
|
90
|
-
|
|
91
|
-
logger.info("MCP 加载成功,当前可用工具列表为:%s", tool_list)
|
|
90
|
+
logger.info("[MCP] 加载成功,当前可用工具列表为:%s", tool_list)
|
|
92
91
|
|
|
93
92
|
elif method == "tools/call":
|
|
94
93
|
tool_name = payload["params"]["name"]
|
|
95
94
|
if not self.tool_func.get(tool_name):
|
|
96
|
-
|
|
95
|
+
logger.warning("[MCP] Tool not found: %s", tool_name)
|
|
96
|
+
return
|
|
97
|
+
|
|
97
98
|
mcp_res = await self.mcp_tool_call(payload)
|
|
98
99
|
await self.websocket.send(mcp_res)
|
|
100
|
+
logger.info("[MCP] Tool %s called", tool_name)
|
|
99
101
|
else:
|
|
100
|
-
logger.warning("unknown method %s: %s", method, payload)
|
|
102
|
+
logger.warning("[MCP] unknown method %s: %s", method, payload)
|
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xiaozhi-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.4
|
|
4
4
|
Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
Author-email: dairoot <623815825@qq.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/dairoot/xiaozhi-sdk
|
|
8
8
|
Classifier: Programming Language :: Python :: 3
|
|
9
9
|
Classifier: License :: OSI Approved :: MIT License
|
|
10
10
|
Classifier: Operating System :: OS Independent
|
|
11
|
-
Requires-Python: >=3.8
|
|
11
|
+
Requires-Python: >=3.8.1
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
@@ -19,16 +19,7 @@ Requires-Dist: opuslib
|
|
|
19
19
|
Requires-Dist: requests
|
|
20
20
|
Requires-Dist: sounddevice
|
|
21
21
|
Requires-Dist: python-socks
|
|
22
|
-
Dynamic: author
|
|
23
|
-
Dynamic: author-email
|
|
24
|
-
Dynamic: classifier
|
|
25
|
-
Dynamic: description
|
|
26
|
-
Dynamic: description-content-type
|
|
27
|
-
Dynamic: home-page
|
|
28
22
|
Dynamic: license-file
|
|
29
|
-
Dynamic: requires-dist
|
|
30
|
-
Dynamic: requires-python
|
|
31
|
-
Dynamic: summary
|
|
32
23
|
|
|
33
24
|
# 小智SDK (XiaoZhi SDK)
|
|
34
25
|
|
|
@@ -71,9 +62,12 @@ positional arguments:
|
|
|
71
62
|
device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
72
63
|
|
|
73
64
|
options:
|
|
74
|
-
-h, --help
|
|
75
|
-
--url URL
|
|
76
|
-
--ota_url OTA_URL
|
|
65
|
+
-h, --help show this help message and exit
|
|
66
|
+
--url URL 服务端websocket地址
|
|
67
|
+
--ota_url OTA_URL OTA地址
|
|
68
|
+
--serial_number SERIAL_NUMBER 设备的序列号
|
|
69
|
+
--license_key LICENSE_KEY 设备的授权密钥
|
|
70
|
+
|
|
77
71
|
```
|
|
78
72
|
|
|
79
73
|
#### 连接设备(需要提供 MAC 地址)
|
xiaozhi_sdk-0.0.2/setup.cfg
DELETED
|
@@ -1,17 +0,0 @@
|
|
|
1
|
-
[flake8]
|
|
2
|
-
ignore = E203, E266, E501, W503, F403, F401, E731, E711, C901, F841, D103, B008, D400, E402, E712
|
|
3
|
-
max-line-length = 120
|
|
4
|
-
max-complexity = 20
|
|
5
|
-
exclude =
|
|
6
|
-
tests/*
|
|
7
|
-
|
|
8
|
-
[coverage:run]
|
|
9
|
-
omit =
|
|
10
|
-
xiaozhi_sdk/__main__.py
|
|
11
|
-
tests/*
|
|
12
|
-
setup.py
|
|
13
|
-
|
|
14
|
-
[egg_info]
|
|
15
|
-
tag_build =
|
|
16
|
-
tag_date = 0
|
|
17
|
-
|
xiaozhi_sdk-0.0.2/setup.py
DELETED
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
import os
|
|
2
|
-
|
|
3
|
-
from setuptools import find_packages, setup
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
# 收集指定目录下的所有文件
|
|
7
|
-
def get_data_files(*directories):
|
|
8
|
-
data_files = []
|
|
9
|
-
for directory in directories:
|
|
10
|
-
if os.path.exists(directory):
|
|
11
|
-
for root, _, files in os.walk(directory):
|
|
12
|
-
for file in files:
|
|
13
|
-
# 将相对于项目根目录的路径转换为相对于包的路径
|
|
14
|
-
rel_path = os.path.relpath(os.path.join(root, file), ".")
|
|
15
|
-
data_files.append(rel_path)
|
|
16
|
-
return data_files
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
setup(
|
|
20
|
-
name="xiaozhi-sdk", # 包名
|
|
21
|
-
version="0.0.2", # 版本号
|
|
22
|
-
packages=find_packages(), # 自动发现包
|
|
23
|
-
package_data={
|
|
24
|
-
"xiaozhi_sdk": ["../" + f for f in get_data_files("file")], # 包含file目录下的所有文件
|
|
25
|
-
},
|
|
26
|
-
include_package_data=True, # 使用MANIFEST.in文件包含额外文件
|
|
27
|
-
install_requires=[ # 依赖
|
|
28
|
-
"numpy",
|
|
29
|
-
"websockets",
|
|
30
|
-
"aiohttp",
|
|
31
|
-
"av",
|
|
32
|
-
"opuslib",
|
|
33
|
-
"requests",
|
|
34
|
-
"sounddevice",
|
|
35
|
-
"python-socks",
|
|
36
|
-
],
|
|
37
|
-
author="dairoot",
|
|
38
|
-
author_email="623815825@qq.com", # 作者邮箱
|
|
39
|
-
description="一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。", # 简短描述
|
|
40
|
-
long_description=open("README.md").read(), # 详细描述(通常从 README 读取)
|
|
41
|
-
long_description_content_type="text/markdown", # README 文件格式
|
|
42
|
-
url="https://github.com/dairoot/xiaozhi-sdk", # 项目主页
|
|
43
|
-
classifiers=[ # 分类元数据
|
|
44
|
-
"Programming Language :: Python :: 3",
|
|
45
|
-
"License :: OSI Approved :: MIT License",
|
|
46
|
-
"Operating System :: OS Independent",
|
|
47
|
-
],
|
|
48
|
-
python_requires=">=3.8", # 支持的 Python 版本
|
|
49
|
-
)
|
|
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
|
|
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
|