xiaozhi-sdk 0.0.4__py3-none-any.whl → 0.0.6__py3-none-any.whl
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.
- xiaozhi_sdk/__init__.py +2 -221
- xiaozhi_sdk/__main__.py +3 -115
- xiaozhi_sdk/cli.py +137 -0
- xiaozhi_sdk/config.py +1 -1
- xiaozhi_sdk/core.py +257 -0
- xiaozhi_sdk/iot.py +8 -2
- xiaozhi_sdk/mcp.py +2 -2
- xiaozhi_sdk/opus.py +1 -1
- {xiaozhi_sdk-0.0.4.dist-info → xiaozhi_sdk-0.0.6.dist-info}/METADATA +9 -19
- {xiaozhi_sdk-0.0.4.dist-info → xiaozhi_sdk-0.0.6.dist-info}/RECORD +13 -11
- {xiaozhi_sdk-0.0.4.dist-info → xiaozhi_sdk-0.0.6.dist-info}/WHEEL +0 -0
- {xiaozhi_sdk-0.0.4.dist-info → xiaozhi_sdk-0.0.6.dist-info}/licenses/LICENSE +0 -0
- {xiaozhi_sdk-0.0.4.dist-info → xiaozhi_sdk-0.0.6.dist-info}/top_level.txt +0 -0
xiaozhi_sdk/__init__.py
CHANGED
|
@@ -1,222 +1,3 @@
|
|
|
1
|
-
__version__ = "0.0.
|
|
1
|
+
__version__ = "0.0.6"
|
|
2
2
|
|
|
3
|
-
import
|
|
4
|
-
import json
|
|
5
|
-
import logging
|
|
6
|
-
import os
|
|
7
|
-
import re
|
|
8
|
-
import uuid
|
|
9
|
-
from collections import deque
|
|
10
|
-
from typing import Any, Callable, Dict, Optional
|
|
11
|
-
|
|
12
|
-
import websockets
|
|
13
|
-
|
|
14
|
-
from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
|
|
15
|
-
from xiaozhi_sdk.iot import OtaDevice
|
|
16
|
-
from xiaozhi_sdk.mcp import McpTool
|
|
17
|
-
from xiaozhi_sdk.utils import get_wav_info, read_audio_file, setup_opus
|
|
18
|
-
|
|
19
|
-
setup_opus()
|
|
20
|
-
from xiaozhi_sdk.opus import AudioOpus
|
|
21
|
-
|
|
22
|
-
logger = logging.getLogger("xiaozhi_sdk")
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
class XiaoZhiWebsocket(McpTool):
|
|
26
|
-
|
|
27
|
-
def __init__(
|
|
28
|
-
self,
|
|
29
|
-
message_handler_callback: Optional[Callable] = None,
|
|
30
|
-
url: Optional[str] = None,
|
|
31
|
-
ota_url: Optional[str] = None,
|
|
32
|
-
audio_sample_rate: int = 16000,
|
|
33
|
-
audio_channels: int = 1,
|
|
34
|
-
):
|
|
35
|
-
super().__init__()
|
|
36
|
-
self.url = url
|
|
37
|
-
self.ota_url = ota_url
|
|
38
|
-
self.audio_channels = audio_channels
|
|
39
|
-
self.audio_opus = AudioOpus(audio_sample_rate, audio_channels)
|
|
40
|
-
|
|
41
|
-
# 客户端标识
|
|
42
|
-
self.client_id = str(uuid.uuid4())
|
|
43
|
-
self.mac_addr: Optional[str] = None
|
|
44
|
-
|
|
45
|
-
# 回调函数
|
|
46
|
-
self.message_handler_callback = message_handler_callback
|
|
47
|
-
|
|
48
|
-
# 连接状态
|
|
49
|
-
self.hello_received = asyncio.Event()
|
|
50
|
-
self.session_id = ""
|
|
51
|
-
self.websocket = None
|
|
52
|
-
self.message_handler_task: Optional[asyncio.Task] = None
|
|
53
|
-
|
|
54
|
-
# 输出音频
|
|
55
|
-
self.output_audio_queue: deque[bytes] = deque()
|
|
56
|
-
|
|
57
|
-
# OTA设备
|
|
58
|
-
self.ota: Optional[OtaDevice] = None
|
|
59
|
-
|
|
60
|
-
async def _send_hello(self, aec: bool) -> None:
|
|
61
|
-
"""发送hello消息"""
|
|
62
|
-
hello_message = {
|
|
63
|
-
"type": "hello",
|
|
64
|
-
"version": 1,
|
|
65
|
-
"features": {"aec": aec, "mcp": True},
|
|
66
|
-
"transport": "websocket",
|
|
67
|
-
"audio_params": {
|
|
68
|
-
"format": "opus",
|
|
69
|
-
"sample_rate": INPUT_SERVER_AUDIO_SAMPLE_RATE,
|
|
70
|
-
"channels": 1,
|
|
71
|
-
"frame_duration": 60,
|
|
72
|
-
},
|
|
73
|
-
}
|
|
74
|
-
await self.websocket.send(json.dumps(hello_message))
|
|
75
|
-
await asyncio.wait_for(self.hello_received.wait(), timeout=10.0)
|
|
76
|
-
|
|
77
|
-
async def _start_listen(self) -> None:
|
|
78
|
-
"""开始监听"""
|
|
79
|
-
|
|
80
|
-
listen_message = {"session_id": self.session_id, "type": "listen", "state": "start", "mode": "realtime"}
|
|
81
|
-
await self.websocket.send(json.dumps(listen_message))
|
|
82
|
-
|
|
83
|
-
async def _activate_iot_device(self, license_key: str, ota_info: Dict[str, Any]) -> None:
|
|
84
|
-
"""激活IoT设备"""
|
|
85
|
-
if not ota_info.get("activation"):
|
|
86
|
-
return
|
|
87
|
-
|
|
88
|
-
if not self.ota:
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
await self._send_demo_audio()
|
|
92
|
-
challenge = ota_info["activation"]["challenge"]
|
|
93
|
-
await asyncio.sleep(3)
|
|
94
|
-
|
|
95
|
-
for _ in range(10):
|
|
96
|
-
if await self.ota.check_activate(challenge, license_key):
|
|
97
|
-
break
|
|
98
|
-
await asyncio.sleep(3)
|
|
99
|
-
|
|
100
|
-
async def _send_demo_audio(self) -> None:
|
|
101
|
-
"""发送演示音频"""
|
|
102
|
-
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
103
|
-
wav_path = os.path.join(current_dir, "../file/audio/greet.wav")
|
|
104
|
-
framerate, channels = get_wav_info(wav_path)
|
|
105
|
-
audio_opus = AudioOpus(framerate, channels)
|
|
106
|
-
|
|
107
|
-
for pcm_data in read_audio_file(wav_path):
|
|
108
|
-
opus_data = await audio_opus.pcm_to_opus(pcm_data)
|
|
109
|
-
await self.websocket.send(opus_data)
|
|
110
|
-
await self.send_silence_audio()
|
|
111
|
-
|
|
112
|
-
async def send_silence_audio(self, duration_seconds: float = 1.2) -> None:
|
|
113
|
-
"""发送静音音频"""
|
|
114
|
-
frames_count = int(duration_seconds * 1000 / 60)
|
|
115
|
-
pcm_frame = b"\x00\x00" * int(INPUT_SERVER_AUDIO_SAMPLE_RATE / 1000 * 60)
|
|
116
|
-
|
|
117
|
-
for _ in range(frames_count):
|
|
118
|
-
await self.send_audio(pcm_frame)
|
|
119
|
-
|
|
120
|
-
async def _handle_websocket_message(self, message: Any) -> None:
|
|
121
|
-
"""处理接受到的WebSocket消息"""
|
|
122
|
-
|
|
123
|
-
# audio data
|
|
124
|
-
if isinstance(message, bytes):
|
|
125
|
-
pcm_array = await self.audio_opus.opus_to_pcm(message)
|
|
126
|
-
self.output_audio_queue.extend(pcm_array)
|
|
127
|
-
return
|
|
128
|
-
|
|
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)
|
|
139
|
-
|
|
140
|
-
async def _message_handler(self) -> None:
|
|
141
|
-
"""消息处理器"""
|
|
142
|
-
try:
|
|
143
|
-
async for message in self.websocket:
|
|
144
|
-
await self._handle_websocket_message(message)
|
|
145
|
-
except websockets.ConnectionClosed:
|
|
146
|
-
if self.message_handler_callback:
|
|
147
|
-
await self.message_handler_callback(
|
|
148
|
-
{"type": "websocket", "state": "close", "source": "sdk.message_handler"}
|
|
149
|
-
)
|
|
150
|
-
logger.info("[websocket] close")
|
|
151
|
-
|
|
152
|
-
async def set_mcp_tool_callback(self, tool_func: Dict[str, Callable[..., Any]]) -> None:
|
|
153
|
-
"""设置MCP工具回调函数"""
|
|
154
|
-
self.tool_func = tool_func
|
|
155
|
-
|
|
156
|
-
async def init_connection(
|
|
157
|
-
self, mac_addr: str, aec: bool = False, serial_number: str = "", license_key: str = ""
|
|
158
|
-
) -> None:
|
|
159
|
-
"""初始化连接"""
|
|
160
|
-
# 校验MAC地址格式 XX:XX:XX:XX:XX:XX
|
|
161
|
-
mac_pattern = r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
|
|
162
|
-
if not re.match(mac_pattern, mac_addr):
|
|
163
|
-
raise ValueError(f"无效的MAC地址格式: {mac_addr}。正确格式应为 XX:XX:XX:XX:XX:XX")
|
|
164
|
-
|
|
165
|
-
self.mac_addr = mac_addr.lower()
|
|
166
|
-
|
|
167
|
-
self.ota = OtaDevice(self.mac_addr, self.client_id, self.ota_url, serial_number)
|
|
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)
|
|
174
|
-
|
|
175
|
-
headers = {
|
|
176
|
-
"Authorization": "Bearer {}".format(ota_info["websocket"]["token"]),
|
|
177
|
-
"Protocol-Version": "1",
|
|
178
|
-
"Device-Id": self.mac_addr,
|
|
179
|
-
"Client-Id": self.client_id,
|
|
180
|
-
}
|
|
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
|
|
186
|
-
self.message_handler_task = asyncio.create_task(self._message_handler())
|
|
187
|
-
|
|
188
|
-
await self._send_hello(aec)
|
|
189
|
-
await self._start_listen()
|
|
190
|
-
asyncio.create_task(self._activate_iot_device(license_key, ota_info))
|
|
191
|
-
await asyncio.sleep(0.5)
|
|
192
|
-
|
|
193
|
-
async def send_audio(self, pcm: bytes) -> None:
|
|
194
|
-
"""发送音频数据"""
|
|
195
|
-
if not self.websocket:
|
|
196
|
-
return
|
|
197
|
-
|
|
198
|
-
state = self.websocket.state
|
|
199
|
-
if state == websockets.protocol.State.OPEN:
|
|
200
|
-
opus_data = await self.audio_opus.pcm_to_opus(pcm)
|
|
201
|
-
await self.websocket.send(opus_data)
|
|
202
|
-
elif state in [websockets.protocol.State.CLOSED, websockets.protocol.State.CLOSING]:
|
|
203
|
-
if self.message_handler_callback:
|
|
204
|
-
await self.message_handler_callback({"type": "websocket", "state": "close", "source": "sdk.send_audio"})
|
|
205
|
-
self.websocket = None
|
|
206
|
-
logger.info("[websocket] close")
|
|
207
|
-
|
|
208
|
-
await asyncio.sleep(0.5)
|
|
209
|
-
else:
|
|
210
|
-
await asyncio.sleep(0.1)
|
|
211
|
-
|
|
212
|
-
async def close(self) -> None:
|
|
213
|
-
"""关闭连接"""
|
|
214
|
-
if self.message_handler_task and not self.message_handler_task.done():
|
|
215
|
-
self.message_handler_task.cancel()
|
|
216
|
-
try:
|
|
217
|
-
await self.message_handler_task
|
|
218
|
-
except asyncio.CancelledError:
|
|
219
|
-
pass
|
|
220
|
-
|
|
221
|
-
if self.websocket:
|
|
222
|
-
await self.websocket.close()
|
|
3
|
+
from xiaozhi_sdk.core import XiaoZhiWebsocket # noqa
|
xiaozhi_sdk/__main__.py
CHANGED
|
@@ -1,123 +1,11 @@
|
|
|
1
|
-
import argparse
|
|
2
|
-
import asyncio
|
|
3
1
|
import logging
|
|
4
|
-
import time
|
|
5
|
-
from collections import deque
|
|
6
|
-
from typing import Optional
|
|
7
2
|
|
|
8
|
-
|
|
9
|
-
import sounddevice as sd
|
|
3
|
+
from xiaozhi_sdk.cli import main
|
|
10
4
|
|
|
11
|
-
from xiaozhi_sdk import XiaoZhiWebsocket
|
|
12
|
-
from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
|
|
13
|
-
|
|
14
|
-
# 配置logging
|
|
15
|
-
logging.basicConfig(
|
|
16
|
-
level=logging.INFO, format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", datefmt="%Y-%m-%d %H:%M:%S"
|
|
17
|
-
)
|
|
18
5
|
logger = logging.getLogger("xiaozhi_sdk")
|
|
19
6
|
|
|
20
|
-
# 全局状态
|
|
21
|
-
input_audio_buffer: deque[bytes] = deque()
|
|
22
|
-
is_playing_audio = False
|
|
23
|
-
is_end = False
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
async def handle_message(message):
|
|
27
|
-
"""处理接收到的消息"""
|
|
28
|
-
global is_end
|
|
29
|
-
logger.info("message received: %s", message)
|
|
30
|
-
if message["type"] == "websocket" and message["state"] == "close":
|
|
31
|
-
is_end = True
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
async def play_assistant_audio(audio_queue: deque[bytes]):
|
|
35
|
-
"""播放音频流"""
|
|
36
|
-
global is_playing_audio
|
|
37
|
-
|
|
38
|
-
stream = sd.OutputStream(samplerate=INPUT_SERVER_AUDIO_SAMPLE_RATE, channels=1, dtype=np.int16)
|
|
39
|
-
stream.start()
|
|
40
|
-
last_audio_time = None
|
|
41
|
-
|
|
42
|
-
while True:
|
|
43
|
-
if is_end:
|
|
44
|
-
return
|
|
45
|
-
|
|
46
|
-
if not audio_queue:
|
|
47
|
-
await asyncio.sleep(0.01)
|
|
48
|
-
if last_audio_time and time.time() - last_audio_time > 1:
|
|
49
|
-
is_playing_audio = False
|
|
50
|
-
continue
|
|
51
|
-
|
|
52
|
-
is_playing_audio = True
|
|
53
|
-
pcm_data = audio_queue.popleft()
|
|
54
|
-
stream.write(pcm_data)
|
|
55
|
-
last_audio_time = time.time()
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
class XiaoZhiClient:
|
|
59
|
-
"""小智客户端类"""
|
|
60
|
-
|
|
61
|
-
def __init__(
|
|
62
|
-
self,
|
|
63
|
-
url: Optional[str] = None,
|
|
64
|
-
ota_url: Optional[str] = None,
|
|
65
|
-
):
|
|
66
|
-
self.xiaozhi: Optional[XiaoZhiWebsocket] = None
|
|
67
|
-
self.url = url
|
|
68
|
-
self.ota_url = ota_url
|
|
69
|
-
|
|
70
|
-
async def start(self, mac_address: str, serial_number: str = "", license_key: str = ""):
|
|
71
|
-
"""启动客户端连接"""
|
|
72
|
-
self.mac_address = mac_address
|
|
73
|
-
self.xiaozhi = XiaoZhiWebsocket(handle_message, url=self.url, ota_url=self.ota_url)
|
|
74
|
-
await self.xiaozhi.init_connection(
|
|
75
|
-
self.mac_address, aec=False, serial_number=serial_number, license_key=license_key
|
|
76
|
-
)
|
|
77
|
-
asyncio.create_task(play_assistant_audio(self.xiaozhi.output_audio_queue))
|
|
78
|
-
|
|
79
|
-
def audio_callback(self, indata, frames, time, status):
|
|
80
|
-
"""音频输入回调函数"""
|
|
81
|
-
pcm_data = (indata.flatten() * 32767).astype(np.int16).tobytes()
|
|
82
|
-
input_audio_buffer.append(pcm_data)
|
|
83
|
-
|
|
84
|
-
async def process_audio_input(self):
|
|
85
|
-
"""处理音频输入"""
|
|
86
|
-
while True:
|
|
87
|
-
|
|
88
|
-
if is_end:
|
|
89
|
-
return
|
|
90
|
-
|
|
91
|
-
if not input_audio_buffer:
|
|
92
|
-
await asyncio.sleep(0.02)
|
|
93
|
-
continue
|
|
94
|
-
|
|
95
|
-
pcm_data = input_audio_buffer.popleft()
|
|
96
|
-
if not is_playing_audio:
|
|
97
|
-
await self.xiaozhi.send_audio(pcm_data)
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
async def main():
|
|
101
|
-
"""主函数"""
|
|
102
|
-
parser = argparse.ArgumentParser(description="小智SDK客户端")
|
|
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="设备的授权密钥")
|
|
109
|
-
|
|
110
|
-
args = parser.parse_args()
|
|
111
|
-
logger.info("Recording... Press Ctrl+C to stop.")
|
|
112
|
-
client = XiaoZhiClient(args.url, args.ota_url)
|
|
113
|
-
await client.start(args.device, args.serial_number, args.license_key)
|
|
114
|
-
|
|
115
|
-
with sd.InputStream(callback=client.audio_callback, channels=1, samplerate=16000, blocksize=960):
|
|
116
|
-
await client.process_audio_input()
|
|
117
|
-
|
|
118
|
-
|
|
119
7
|
if __name__ == "__main__":
|
|
120
8
|
try:
|
|
121
|
-
|
|
9
|
+
main()
|
|
122
10
|
except KeyboardInterrupt:
|
|
123
|
-
logger.
|
|
11
|
+
logger.debug("Stopping...")
|
xiaozhi_sdk/cli.py
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import time
|
|
4
|
+
from collections import deque
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
import click
|
|
8
|
+
import colorlog
|
|
9
|
+
import numpy as np
|
|
10
|
+
import sounddevice as sd
|
|
11
|
+
|
|
12
|
+
from xiaozhi_sdk import XiaoZhiWebsocket
|
|
13
|
+
from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
|
|
14
|
+
|
|
15
|
+
# 配置彩色logging
|
|
16
|
+
handler = colorlog.StreamHandler()
|
|
17
|
+
handler.setFormatter(
|
|
18
|
+
colorlog.ColoredFormatter(
|
|
19
|
+
"%(log_color)s%(asctime)s - %(name)s - %(levelname)s - %(message)s",
|
|
20
|
+
datefmt="%Y-%m-%d %H:%M:%S",
|
|
21
|
+
log_colors={
|
|
22
|
+
"DEBUG": "green",
|
|
23
|
+
"INFO": "white",
|
|
24
|
+
"WARNING": "yellow",
|
|
25
|
+
"ERROR": "red",
|
|
26
|
+
"CRITICAL": "red,bg_white",
|
|
27
|
+
},
|
|
28
|
+
)
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
logger = logging.getLogger("xiaozhi_sdk")
|
|
32
|
+
logger.addHandler(handler)
|
|
33
|
+
logger.setLevel(logging.DEBUG)
|
|
34
|
+
|
|
35
|
+
# 全局状态
|
|
36
|
+
input_audio_buffer: deque[bytes] = deque()
|
|
37
|
+
is_playing_audio = False
|
|
38
|
+
is_end = False
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def handle_message(message):
|
|
42
|
+
"""处理接收到的消息"""
|
|
43
|
+
global is_end
|
|
44
|
+
logger.info("message received: %s", message)
|
|
45
|
+
if message["type"] == "websocket" and message["state"] == "close":
|
|
46
|
+
is_end = True
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
async def play_assistant_audio(audio_queue: deque[bytes]):
|
|
50
|
+
"""播放音频流"""
|
|
51
|
+
global is_playing_audio
|
|
52
|
+
|
|
53
|
+
stream = sd.OutputStream(samplerate=INPUT_SERVER_AUDIO_SAMPLE_RATE, channels=1, dtype=np.int16)
|
|
54
|
+
stream.start()
|
|
55
|
+
last_audio_time = None
|
|
56
|
+
|
|
57
|
+
while True:
|
|
58
|
+
if is_end:
|
|
59
|
+
return
|
|
60
|
+
|
|
61
|
+
if not audio_queue:
|
|
62
|
+
await asyncio.sleep(0.01)
|
|
63
|
+
if last_audio_time and time.time() - last_audio_time > 1:
|
|
64
|
+
is_playing_audio = False
|
|
65
|
+
continue
|
|
66
|
+
|
|
67
|
+
is_playing_audio = True
|
|
68
|
+
pcm_data = audio_queue.popleft()
|
|
69
|
+
stream.write(pcm_data)
|
|
70
|
+
last_audio_time = time.time()
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
class XiaoZhiClient:
|
|
74
|
+
"""小智客户端类"""
|
|
75
|
+
|
|
76
|
+
def __init__(
|
|
77
|
+
self,
|
|
78
|
+
url: Optional[str] = None,
|
|
79
|
+
ota_url: Optional[str] = None,
|
|
80
|
+
):
|
|
81
|
+
self.xiaozhi: Optional[XiaoZhiWebsocket] = None
|
|
82
|
+
self.url = url
|
|
83
|
+
self.ota_url = ota_url
|
|
84
|
+
self.mac_address = ""
|
|
85
|
+
|
|
86
|
+
async def start(self, mac_address: str, serial_number: str = "", license_key: str = ""):
|
|
87
|
+
"""启动客户端连接"""
|
|
88
|
+
self.mac_address = mac_address
|
|
89
|
+
self.xiaozhi = XiaoZhiWebsocket(handle_message, url=self.url, ota_url=self.ota_url, send_wake=True)
|
|
90
|
+
await self.xiaozhi.init_connection(
|
|
91
|
+
self.mac_address, aec=False, serial_number=serial_number, license_key=license_key
|
|
92
|
+
)
|
|
93
|
+
asyncio.create_task(play_assistant_audio(self.xiaozhi.output_audio_queue))
|
|
94
|
+
|
|
95
|
+
def audio_callback(self, indata, frames, time, status):
|
|
96
|
+
"""音频输入回调函数"""
|
|
97
|
+
pcm_data = (indata.flatten() * 32767).astype(np.int16).tobytes()
|
|
98
|
+
input_audio_buffer.append(pcm_data)
|
|
99
|
+
|
|
100
|
+
async def process_audio_input(self):
|
|
101
|
+
"""处理音频输入"""
|
|
102
|
+
while True:
|
|
103
|
+
|
|
104
|
+
if is_end:
|
|
105
|
+
return
|
|
106
|
+
|
|
107
|
+
if not input_audio_buffer:
|
|
108
|
+
await asyncio.sleep(0.02)
|
|
109
|
+
continue
|
|
110
|
+
|
|
111
|
+
pcm_data = input_audio_buffer.popleft()
|
|
112
|
+
if not is_playing_audio:
|
|
113
|
+
await self.xiaozhi.send_audio(pcm_data)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
async def run_client(mac_address: str, url: str, ota_url: str, serial_number: str, license_key: str):
|
|
117
|
+
"""运行客户端的异步函数"""
|
|
118
|
+
logger.debug("Recording... Press Ctrl+C to stop.")
|
|
119
|
+
client = XiaoZhiClient(url, ota_url)
|
|
120
|
+
await client.start(mac_address, serial_number, license_key)
|
|
121
|
+
|
|
122
|
+
with sd.InputStream(callback=client.audio_callback, channels=1, samplerate=16000, blocksize=960):
|
|
123
|
+
await client.process_audio_input()
|
|
124
|
+
|
|
125
|
+
|
|
126
|
+
@click.command()
|
|
127
|
+
@click.argument("mac_address")
|
|
128
|
+
@click.option("--url", help="服务端websocket地址")
|
|
129
|
+
@click.option("--ota_url", help="OTA地址")
|
|
130
|
+
@click.option("--serial_number", default="", help="设备的序列号")
|
|
131
|
+
@click.option("--license_key", default="", help="设备的授权密钥")
|
|
132
|
+
def main(mac_address: str, url: str, ota_url: str, serial_number: str, license_key: str):
|
|
133
|
+
"""小智SDK客户端
|
|
134
|
+
|
|
135
|
+
MAC_ADDRESS: 设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
136
|
+
"""
|
|
137
|
+
asyncio.run(run_client(mac_address, url, ota_url, serial_number, license_key))
|
xiaozhi_sdk/config.py
CHANGED
xiaozhi_sdk/core.py
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import json
|
|
3
|
+
import logging
|
|
4
|
+
import os
|
|
5
|
+
import re
|
|
6
|
+
import uuid
|
|
7
|
+
from collections import deque
|
|
8
|
+
from typing import Any, Callable, Dict, Optional
|
|
9
|
+
|
|
10
|
+
import websockets
|
|
11
|
+
|
|
12
|
+
from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
|
|
13
|
+
from xiaozhi_sdk.iot import OtaDevice
|
|
14
|
+
from xiaozhi_sdk.mcp import McpTool
|
|
15
|
+
from xiaozhi_sdk.utils import get_wav_info, read_audio_file, setup_opus
|
|
16
|
+
|
|
17
|
+
setup_opus()
|
|
18
|
+
from xiaozhi_sdk.opus import AudioOpus
|
|
19
|
+
|
|
20
|
+
logger = logging.getLogger("xiaozhi_sdk")
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class XiaoZhiWebsocket(McpTool):
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
message_handler_callback: Optional[Callable] = None,
|
|
28
|
+
url: Optional[str] = None,
|
|
29
|
+
ota_url: Optional[str] = None,
|
|
30
|
+
audio_sample_rate: int = 16000,
|
|
31
|
+
audio_channels: int = 1,
|
|
32
|
+
send_wake: bool = False,
|
|
33
|
+
):
|
|
34
|
+
super().__init__()
|
|
35
|
+
self.url = url
|
|
36
|
+
self.ota_url = ota_url
|
|
37
|
+
self.send_wake = send_wake
|
|
38
|
+
self.audio_channels = audio_channels
|
|
39
|
+
self.audio_opus = AudioOpus(audio_sample_rate, audio_channels)
|
|
40
|
+
|
|
41
|
+
# 客户端标识
|
|
42
|
+
self.client_id = str(uuid.uuid4())
|
|
43
|
+
self.mac_addr: Optional[str] = None
|
|
44
|
+
self.aec = False
|
|
45
|
+
self.websocket_token = ""
|
|
46
|
+
|
|
47
|
+
# 回调函数
|
|
48
|
+
self.message_handler_callback = message_handler_callback
|
|
49
|
+
|
|
50
|
+
# 连接状态
|
|
51
|
+
self.hello_received = asyncio.Event()
|
|
52
|
+
self.session_id = ""
|
|
53
|
+
self.websocket = None
|
|
54
|
+
self.message_handler_task: Optional[asyncio.Task] = None
|
|
55
|
+
|
|
56
|
+
# 输出音频
|
|
57
|
+
self.output_audio_queue: deque[bytes] = deque()
|
|
58
|
+
|
|
59
|
+
# OTA设备
|
|
60
|
+
self.ota: Optional[OtaDevice] = None
|
|
61
|
+
self.iot_task: Optional[asyncio.Task] = None
|
|
62
|
+
self.wait_device_activated: bool = False
|
|
63
|
+
|
|
64
|
+
async def _send_hello(self, aec: bool) -> None:
|
|
65
|
+
"""发送hello消息"""
|
|
66
|
+
hello_message = {
|
|
67
|
+
"type": "hello",
|
|
68
|
+
"version": 1,
|
|
69
|
+
"features": {"mcp": True, "aec": aec},
|
|
70
|
+
"transport": "websocket",
|
|
71
|
+
"audio_params": {
|
|
72
|
+
"format": "opus",
|
|
73
|
+
"sample_rate": 16000,
|
|
74
|
+
"channels": 1,
|
|
75
|
+
"frame_duration": 60,
|
|
76
|
+
},
|
|
77
|
+
}
|
|
78
|
+
await self.websocket.send(json.dumps(hello_message))
|
|
79
|
+
await asyncio.wait_for(self.hello_received.wait(), timeout=10.0)
|
|
80
|
+
|
|
81
|
+
async def _start_listen(self) -> None:
|
|
82
|
+
"""开始监听"""
|
|
83
|
+
listen_message = {"session_id": self.session_id, "type": "listen", "state": "start", "mode": "realtime"}
|
|
84
|
+
await self.websocket.send(json.dumps(listen_message))
|
|
85
|
+
|
|
86
|
+
async def is_activate(self, ota_info):
|
|
87
|
+
"""是否激活"""
|
|
88
|
+
if ota_info.get("activation"):
|
|
89
|
+
return False
|
|
90
|
+
|
|
91
|
+
return True
|
|
92
|
+
|
|
93
|
+
async def _activate_iot_device(self, license_key: str, ota_info: Dict[str, Any]) -> None:
|
|
94
|
+
"""激活IoT设备"""
|
|
95
|
+
if not self.ota:
|
|
96
|
+
return
|
|
97
|
+
|
|
98
|
+
challenge = ota_info["activation"]["challenge"]
|
|
99
|
+
await asyncio.sleep(3)
|
|
100
|
+
self.wait_device_activated = True
|
|
101
|
+
for _ in range(10):
|
|
102
|
+
if await self.ota.check_activate(challenge, license_key):
|
|
103
|
+
self.wait_device_activated = False
|
|
104
|
+
break
|
|
105
|
+
await asyncio.sleep(3)
|
|
106
|
+
|
|
107
|
+
async def _send_demo_audio(self) -> None:
|
|
108
|
+
"""发送演示音频"""
|
|
109
|
+
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
110
|
+
wav_path = os.path.join(current_dir, "../file/audio/greet.wav")
|
|
111
|
+
framerate, channels = get_wav_info(wav_path)
|
|
112
|
+
audio_opus = AudioOpus(framerate, channels)
|
|
113
|
+
|
|
114
|
+
for pcm_data in read_audio_file(wav_path):
|
|
115
|
+
opus_data = await audio_opus.pcm_to_opus(pcm_data)
|
|
116
|
+
await self.websocket.send(opus_data)
|
|
117
|
+
await self.send_silence_audio()
|
|
118
|
+
|
|
119
|
+
async def send_wake_word(self, wake_word: str = "你好,小智") -> None:
|
|
120
|
+
"""发送唤醒词"""
|
|
121
|
+
await self.websocket.send(
|
|
122
|
+
json.dumps({"session_id": self.session_id, "type": "listen", "state": "detect", "text": wake_word})
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
async def send_silence_audio(self, duration_seconds: float = 1.2) -> None:
|
|
126
|
+
"""发送静音音频"""
|
|
127
|
+
frames_count = int(duration_seconds * 1000 / 60)
|
|
128
|
+
pcm_frame = b"\x00\x00" * int(INPUT_SERVER_AUDIO_SAMPLE_RATE / 1000 * 60)
|
|
129
|
+
|
|
130
|
+
for _ in range(frames_count):
|
|
131
|
+
await self.send_audio(pcm_frame)
|
|
132
|
+
|
|
133
|
+
async def _handle_websocket_message(self, message: Any) -> None:
|
|
134
|
+
"""处理接受到的WebSocket消息"""
|
|
135
|
+
|
|
136
|
+
# audio data
|
|
137
|
+
if isinstance(message, bytes):
|
|
138
|
+
pcm_array = await self.audio_opus.opus_to_pcm(message)
|
|
139
|
+
self.output_audio_queue.extend(pcm_array)
|
|
140
|
+
return
|
|
141
|
+
|
|
142
|
+
# json message
|
|
143
|
+
data = json.loads(message)
|
|
144
|
+
message_type = data["type"]
|
|
145
|
+
if message_type == "hello":
|
|
146
|
+
self.hello_received.set()
|
|
147
|
+
self.session_id = data["session_id"]
|
|
148
|
+
elif message_type == "mcp":
|
|
149
|
+
await self.mcp(data)
|
|
150
|
+
elif self.message_handler_callback:
|
|
151
|
+
await self.message_handler_callback(data)
|
|
152
|
+
|
|
153
|
+
async def _message_handler(self) -> None:
|
|
154
|
+
"""消息处理器"""
|
|
155
|
+
try:
|
|
156
|
+
async for message in self.websocket:
|
|
157
|
+
await self._handle_websocket_message(message)
|
|
158
|
+
except websockets.ConnectionClosed:
|
|
159
|
+
if self.message_handler_callback:
|
|
160
|
+
await self.message_handler_callback(
|
|
161
|
+
{"type": "websocket", "state": "close", "source": "sdk.message_handler"}
|
|
162
|
+
)
|
|
163
|
+
logger.debug("[websocket] close")
|
|
164
|
+
|
|
165
|
+
async def set_mcp_tool_callback(self, tool_func: Dict[str, Callable[..., Any]]) -> None:
|
|
166
|
+
"""设置MCP工具回调函数"""
|
|
167
|
+
self.tool_func = tool_func
|
|
168
|
+
|
|
169
|
+
async def connect_websocket(self, websocket_token):
|
|
170
|
+
"""连接websocket"""
|
|
171
|
+
headers = {
|
|
172
|
+
"Authorization": "Bearer {}".format(websocket_token),
|
|
173
|
+
"Protocol-Version": "1",
|
|
174
|
+
"Device-Id": self.mac_addr,
|
|
175
|
+
"Client-Id": self.client_id,
|
|
176
|
+
}
|
|
177
|
+
try:
|
|
178
|
+
self.websocket = await websockets.connect(uri=self.url, additional_headers=headers)
|
|
179
|
+
except websockets.exceptions.InvalidMessage as e:
|
|
180
|
+
logger.error("[websocket] 连接失败,请检查网络连接或设备状态。当前链接地址: %s, 错误信息:%s", self.url, e)
|
|
181
|
+
return
|
|
182
|
+
self.message_handler_task = asyncio.create_task(self._message_handler())
|
|
183
|
+
|
|
184
|
+
await self._send_hello(self.aec)
|
|
185
|
+
await self._start_listen()
|
|
186
|
+
logger.debug("[websocket] Connection successful")
|
|
187
|
+
await asyncio.sleep(0.5)
|
|
188
|
+
|
|
189
|
+
async def init_connection(
|
|
190
|
+
self, mac_addr: str, aec: bool = False, serial_number: str = "", license_key: str = ""
|
|
191
|
+
) -> None:
|
|
192
|
+
"""初始化连接"""
|
|
193
|
+
mac_pattern = r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
|
|
194
|
+
if not re.match(mac_pattern, mac_addr):
|
|
195
|
+
raise ValueError(f"无效的MAC地址格式: {mac_addr}。正确格式应为 XX:XX:XX:XX:XX:XX")
|
|
196
|
+
|
|
197
|
+
self.mac_addr = mac_addr.lower()
|
|
198
|
+
self.aec = aec
|
|
199
|
+
|
|
200
|
+
self.ota = OtaDevice(self.mac_addr, self.client_id, self.ota_url, serial_number)
|
|
201
|
+
ota_info = await self.ota.activate_device()
|
|
202
|
+
ws_url = ota_info.get("websocket", {}).get("url")
|
|
203
|
+
self.url = self.url or ws_url
|
|
204
|
+
|
|
205
|
+
if not self.url:
|
|
206
|
+
logger.warning("[websocket] 未找到websocket链接地址")
|
|
207
|
+
return
|
|
208
|
+
|
|
209
|
+
if "tenclass.net" not in self.url and "xiaozhi.me" not in self.url:
|
|
210
|
+
logger.warning("[websocket] 检测到非官方服务器,当前链接地址: %s", self.url)
|
|
211
|
+
|
|
212
|
+
self.websocket_token = ota_info["websocket"]["token"]
|
|
213
|
+
await self.connect_websocket(self.websocket_token)
|
|
214
|
+
|
|
215
|
+
if not await self.is_activate(ota_info):
|
|
216
|
+
self.iot_task = asyncio.create_task(self._activate_iot_device(license_key, ota_info))
|
|
217
|
+
logger.debug("[IOT] 设备未激活")
|
|
218
|
+
|
|
219
|
+
if self.send_wake:
|
|
220
|
+
await self.send_wake_word()
|
|
221
|
+
|
|
222
|
+
async def send_audio(self, pcm: bytes) -> None:
|
|
223
|
+
"""发送音频数据"""
|
|
224
|
+
if not self.websocket:
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
state = self.websocket.state
|
|
228
|
+
if state == websockets.protocol.State.OPEN:
|
|
229
|
+
opus_data = await self.audio_opus.pcm_to_opus(pcm)
|
|
230
|
+
await self.websocket.send(opus_data)
|
|
231
|
+
elif state in [websockets.protocol.State.CLOSED, websockets.protocol.State.CLOSING]:
|
|
232
|
+
if self.wait_device_activated:
|
|
233
|
+
logger.debug("[websocket] Server actively disconnected, reconnecting...")
|
|
234
|
+
await self.connect_websocket(self.websocket_token)
|
|
235
|
+
elif self.message_handler_callback:
|
|
236
|
+
await self.message_handler_callback({"type": "websocket", "state": "close", "source": "sdk.send_audio"})
|
|
237
|
+
self.websocket = None
|
|
238
|
+
logger.debug("[websocket] Server actively disconnected")
|
|
239
|
+
|
|
240
|
+
await asyncio.sleep(0.5)
|
|
241
|
+
else:
|
|
242
|
+
await asyncio.sleep(0.1)
|
|
243
|
+
|
|
244
|
+
async def close(self) -> None:
|
|
245
|
+
"""关闭连接"""
|
|
246
|
+
if self.message_handler_task and not self.message_handler_task.done():
|
|
247
|
+
self.message_handler_task.cancel()
|
|
248
|
+
try:
|
|
249
|
+
await self.message_handler_task
|
|
250
|
+
except asyncio.CancelledError:
|
|
251
|
+
pass
|
|
252
|
+
|
|
253
|
+
if self.iot_task:
|
|
254
|
+
self.iot_task.cancel()
|
|
255
|
+
|
|
256
|
+
if self.websocket:
|
|
257
|
+
await self.websocket.close()
|
xiaozhi_sdk/iot.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import hashlib
|
|
2
2
|
import hmac
|
|
3
3
|
import json
|
|
4
|
+
import logging
|
|
4
5
|
from typing import Any, Dict, Optional
|
|
5
6
|
|
|
6
7
|
import aiohttp
|
|
@@ -13,6 +14,8 @@ BOARD_TYPE = "xiaozhi-sdk-box"
|
|
|
13
14
|
USER_AGENT = "xiaozhi-sdk/{}".format(__version__)
|
|
14
15
|
BOARD_NAME = "xiaozhi-sdk-{}".format(__version__)
|
|
15
16
|
|
|
17
|
+
logger = logging.getLogger("xiaozhi_sdk")
|
|
18
|
+
|
|
16
19
|
|
|
17
20
|
class OtaDevice:
|
|
18
21
|
"""
|
|
@@ -56,7 +59,7 @@ class OtaDevice:
|
|
|
56
59
|
}
|
|
57
60
|
|
|
58
61
|
async with aiohttp.ClientSession() as session:
|
|
59
|
-
async with session.post(self.ota_url, headers=headers, data=json.dumps(payload)) as response:
|
|
62
|
+
async with session.post(self.ota_url + "/", headers=headers, data=json.dumps(payload)) as response:
|
|
60
63
|
response.raise_for_status()
|
|
61
64
|
return await response.json()
|
|
62
65
|
|
|
@@ -72,4 +75,7 @@ class OtaDevice:
|
|
|
72
75
|
|
|
73
76
|
async with aiohttp.ClientSession() as session:
|
|
74
77
|
async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
|
|
75
|
-
|
|
78
|
+
is_ok = response.status == 200
|
|
79
|
+
if not is_ok:
|
|
80
|
+
logger.debug("[IOT] wait for activate device...")
|
|
81
|
+
return is_ok
|
xiaozhi_sdk/mcp.py
CHANGED
|
@@ -87,7 +87,7 @@ 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
|
-
logger.
|
|
90
|
+
logger.debug("[MCP] 加载成功,当前可用工具列表为:%s", tool_list)
|
|
91
91
|
|
|
92
92
|
elif method == "tools/call":
|
|
93
93
|
tool_name = payload["params"]["name"]
|
|
@@ -97,6 +97,6 @@ class McpTool(object):
|
|
|
97
97
|
|
|
98
98
|
mcp_res = await self.mcp_tool_call(payload)
|
|
99
99
|
await self.websocket.send(mcp_res)
|
|
100
|
-
logger.
|
|
100
|
+
logger.debug("[MCP] Tool %s called", tool_name)
|
|
101
101
|
else:
|
|
102
102
|
logger.warning("[MCP] unknown method %s: %s", method, payload)
|
xiaozhi_sdk/opus.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: xiaozhi-sdk
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.6
|
|
4
4
|
Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
|
|
5
5
|
Author-email: dairoot <623815825@qq.com>
|
|
6
6
|
License: MIT
|
|
@@ -12,13 +12,15 @@ Requires-Python: >=3.8.1
|
|
|
12
12
|
Description-Content-Type: text/markdown
|
|
13
13
|
License-File: LICENSE
|
|
14
14
|
Requires-Dist: numpy
|
|
15
|
-
Requires-Dist: websockets
|
|
15
|
+
Requires-Dist: websockets>=15.0.1
|
|
16
16
|
Requires-Dist: aiohttp
|
|
17
17
|
Requires-Dist: av
|
|
18
18
|
Requires-Dist: opuslib
|
|
19
19
|
Requires-Dist: requests
|
|
20
20
|
Requires-Dist: sounddevice
|
|
21
21
|
Requires-Dist: python-socks
|
|
22
|
+
Requires-Dist: click
|
|
23
|
+
Requires-Dist: colorlog
|
|
22
24
|
Dynamic: license-file
|
|
23
25
|
|
|
24
26
|
# 小智SDK (XiaoZhi SDK)
|
|
@@ -53,21 +55,7 @@ pip install xiaozhi-sdk
|
|
|
53
55
|
#### 查看帮助信息
|
|
54
56
|
|
|
55
57
|
```bash
|
|
56
|
-
python -m xiaozhi_sdk
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
输出示例:
|
|
60
|
-
```text
|
|
61
|
-
positional arguments:
|
|
62
|
-
device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
|
|
63
|
-
|
|
64
|
-
options:
|
|
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
|
-
|
|
58
|
+
python -m xiaozhi_sdk --help
|
|
71
59
|
```
|
|
72
60
|
|
|
73
61
|
#### 连接设备(需要提供 MAC 地址)
|
|
@@ -76,11 +64,13 @@ options:
|
|
|
76
64
|
python -m xiaozhi_sdk 00:22:44:66:88:00
|
|
77
65
|
```
|
|
78
66
|
|
|
79
|
-
### 2. 编程使用
|
|
67
|
+
### 2. 编程使用 (高阶用法)
|
|
80
68
|
参考 [examples](examples/) 文件中的示例代码,可以快速开始使用 SDK。
|
|
81
69
|
|
|
82
70
|
|
|
83
|
-
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## ✅ 运行测试
|
|
84
74
|
|
|
85
75
|
```bash
|
|
86
76
|
pytest tests/
|
|
@@ -7,16 +7,18 @@ file/opus/linux-x64-libopus.so,sha256=FmXJqkxLpDzNFOHYkmOzmsp1hP0eIS5b6x_XfOs-IQ
|
|
|
7
7
|
file/opus/macos-arm64-libopus.dylib,sha256=H7wXwkrGwb-hesMMZGFxWb0Ri1Y4m5GWiKsd8CfOhE8,357584
|
|
8
8
|
file/opus/macos-x64-libopus.dylib,sha256=MqyL_OjwSACF4Xs_-KrGbcScy4IEprr5Rlkk3ddZye8,550856
|
|
9
9
|
file/opus/windows-x86_64-opus.dll,sha256=kLfhioMvbJhOgNMAldpWk3DCZqC5Xd70LRbHnACvAnw,463360
|
|
10
|
-
xiaozhi_sdk/__init__.py,sha256=
|
|
11
|
-
xiaozhi_sdk/__main__.py,sha256=
|
|
12
|
-
xiaozhi_sdk/
|
|
10
|
+
xiaozhi_sdk/__init__.py,sha256=byRv-MwEkq9DRNCv9xwhLSqCfHjgMHDa2BizaN55CPo,77
|
|
11
|
+
xiaozhi_sdk/__main__.py,sha256=i0ZJdHUqAKg9vwZrK_w0TJkzdotTYTK8aUeSPcJc1ks,210
|
|
12
|
+
xiaozhi_sdk/cli.py,sha256=0Hq-wPUv5Hfjzn7pVcCNMe3cbKTwwJytDhXSYivFOt8,4239
|
|
13
|
+
xiaozhi_sdk/config.py,sha256=h4mpMeBf2vT9qYAqCCbGVGmMemkgk98pcXP2Rh4TEFc,89
|
|
14
|
+
xiaozhi_sdk/core.py,sha256=a_-JnLMQgT93O9GMytUNLLot8N3dZbNjan-tsC7GwrY,9447
|
|
13
15
|
xiaozhi_sdk/data.py,sha256=8z8erOjBZFvPSBJlPoyTzRYZ3BuMvnPpAFQCbSxs-48,2522
|
|
14
|
-
xiaozhi_sdk/iot.py,sha256=
|
|
15
|
-
xiaozhi_sdk/mcp.py,sha256=
|
|
16
|
-
xiaozhi_sdk/opus.py,sha256=
|
|
16
|
+
xiaozhi_sdk/iot.py,sha256=IO3SfiuQxucYl_917BCNCwIAv1dajCJI-IFTWwHnSDE,2580
|
|
17
|
+
xiaozhi_sdk/mcp.py,sha256=Q_htzBMunj3-9wARONeCFPTbApQicHWRbG6BlX4oQss,3825
|
|
18
|
+
xiaozhi_sdk/opus.py,sha256=BX5BZATjWSUGbh1z-GjZhIKmZezHPADFYcIpoIsTtRQ,2111
|
|
17
19
|
xiaozhi_sdk/utils.py,sha256=5qHAiI5Nrzeka3TofMPhAVmMovEJJa6QSrKcDM0OF4g,1703
|
|
18
|
-
xiaozhi_sdk-0.0.
|
|
19
|
-
xiaozhi_sdk-0.0.
|
|
20
|
-
xiaozhi_sdk-0.0.
|
|
21
|
-
xiaozhi_sdk-0.0.
|
|
22
|
-
xiaozhi_sdk-0.0.
|
|
20
|
+
xiaozhi_sdk-0.0.6.dist-info/licenses/LICENSE,sha256=Vwgps1iODKl43cAtME_0dawTjAzNW-O2BWiN5BHggww,1085
|
|
21
|
+
xiaozhi_sdk-0.0.6.dist-info/METADATA,sha256=0Mum7x2Ul3YhOeMqk85Rn6SySUVdf2TDfTsyRoQphTM,2014
|
|
22
|
+
xiaozhi_sdk-0.0.6.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
23
|
+
xiaozhi_sdk-0.0.6.dist-info/top_level.txt,sha256=nBpue4hU5Ykm5CtYPsAdxSa_yqbtZsIT_gF_EkBaJPM,12
|
|
24
|
+
xiaozhi_sdk-0.0.6.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|