xiaozhi-sdk 0.0.2__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.

Files changed (32) hide show
  1. xiaozhi_sdk-0.0.2/LICENSE +21 -0
  2. xiaozhi_sdk-0.0.2/MANIFEST.in +5 -0
  3. xiaozhi_sdk-0.0.2/PKG-INFO +100 -0
  4. xiaozhi_sdk-0.0.2/README.md +68 -0
  5. xiaozhi_sdk-0.0.2/file/audio/greet.wav +0 -0
  6. xiaozhi_sdk-0.0.2/file/audio/say_hello.wav +0 -0
  7. xiaozhi_sdk-0.0.2/file/audio/take_photo.wav +0 -0
  8. xiaozhi_sdk-0.0.2/file/image/leijun.jpg +0 -0
  9. xiaozhi_sdk-0.0.2/file/opus/linux-arm64-libopus.so +0 -0
  10. xiaozhi_sdk-0.0.2/file/opus/linux-x64-libopus.so +0 -0
  11. xiaozhi_sdk-0.0.2/file/opus/macos-arm64-libopus.dylib +0 -0
  12. xiaozhi_sdk-0.0.2/file/opus/macos-x64-libopus.dylib +0 -0
  13. xiaozhi_sdk-0.0.2/file/opus/windows-x86_64-opus.dll +0 -0
  14. xiaozhi_sdk-0.0.2/setup.cfg +17 -0
  15. xiaozhi_sdk-0.0.2/setup.py +49 -0
  16. xiaozhi_sdk-0.0.2/tests/test_iot.py +19 -0
  17. xiaozhi_sdk-0.0.2/tests/test_opus.py +9 -0
  18. xiaozhi_sdk-0.0.2/tests/test_pic.py +47 -0
  19. xiaozhi_sdk-0.0.2/tests/test_xiaozhi.py +87 -0
  20. xiaozhi_sdk-0.0.2/xiaozhi_sdk/__init__.py +200 -0
  21. xiaozhi_sdk-0.0.2/xiaozhi_sdk/__main__.py +101 -0
  22. xiaozhi_sdk-0.0.2/xiaozhi_sdk/config.py +3 -0
  23. xiaozhi_sdk-0.0.2/xiaozhi_sdk/data.py +60 -0
  24. xiaozhi_sdk-0.0.2/xiaozhi_sdk/iot.py +76 -0
  25. xiaozhi_sdk-0.0.2/xiaozhi_sdk/mcp.py +100 -0
  26. xiaozhi_sdk-0.0.2/xiaozhi_sdk/opus.py +55 -0
  27. xiaozhi_sdk-0.0.2/xiaozhi_sdk/utils.py +57 -0
  28. xiaozhi_sdk-0.0.2/xiaozhi_sdk.egg-info/PKG-INFO +100 -0
  29. xiaozhi_sdk-0.0.2/xiaozhi_sdk.egg-info/SOURCES.txt +40 -0
  30. xiaozhi_sdk-0.0.2/xiaozhi_sdk.egg-info/dependency_links.txt +1 -0
  31. xiaozhi_sdk-0.0.2/xiaozhi_sdk.egg-info/requires.txt +8 -0
  32. xiaozhi_sdk-0.0.2/xiaozhi_sdk.egg-info/top_level.txt +1 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 dairoot
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,5 @@
1
+ include README.md
2
+ include LICENSE
3
+ recursive-include file *
4
+ recursive-exclude file *.pyc
5
+ recursive-exclude file __pycache__
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: xiaozhi-sdk
3
+ Version: 0.0.2
4
+ Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
5
+ Home-page: https://github.com/dairoot/xiaozhi-sdk
6
+ Author: dairoot
7
+ Author-email: 623815825@qq.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy
15
+ Requires-Dist: websockets
16
+ Requires-Dist: aiohttp
17
+ Requires-Dist: av
18
+ Requires-Dist: opuslib
19
+ Requires-Dist: requests
20
+ Requires-Dist: sounddevice
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
+ Dynamic: license-file
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # 小智SDK (XiaoZhi SDK)
34
+
35
+ [![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
36
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
37
+ [![PyPI](https://img.shields.io/badge/pypi-xiaozhi--sdk-blue.svg)](https://pypi.org/project/xiaozhi-sdk/)
38
+
39
+ 基于虾哥的 [小智esp32 websocket 通讯协议](https://github.com/78/xiaozhi-esp32/blob/main/docs/websocket.md) 实现的 Python SDK。
40
+
41
+ 一个用于连接和控制小智设备的 Python SDK。支持以下功能:
42
+ - 实时音频通信
43
+ - MCP 工具集成
44
+ - 设备管理与控制
45
+
46
+ ---
47
+
48
+ ## 📦 安装
49
+
50
+ ```bash
51
+ pip install xiaozhi-sdk
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 🚀 快速开始
57
+
58
+ ### 1. 终端使用
59
+
60
+ 最简单的方式是通过终端直接连接设备:
61
+
62
+ #### 查看帮助信息
63
+
64
+ ```bash
65
+ python -m xiaozhi_sdk -h
66
+ ```
67
+
68
+ 输出示例:
69
+ ```text
70
+ positional arguments:
71
+ device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
72
+
73
+ options:
74
+ -h, --help 显示帮助信息并退出
75
+ --url URL 小智服务 websocket 地址
76
+ --ota_url OTA_URL 小智 OTA 地址
77
+ ```
78
+
79
+ #### 连接设备(需要提供 MAC 地址)
80
+
81
+ ```bash
82
+ python -m xiaozhi_sdk 00:22:44:66:88:00
83
+ ```
84
+
85
+ ### 2. 编程使用
86
+ 参考 [examples](examples/) 文件中的示例代码,可以快速开始使用 SDK。
87
+
88
+
89
+ ### 运行测试
90
+
91
+ ```bash
92
+ pytest tests/
93
+ ```
94
+
95
+
96
+ ---
97
+
98
+ ## 🫡 致敬
99
+
100
+ - 🫡 虾哥的 [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) 项目
@@ -0,0 +1,68 @@
1
+ # 小智SDK (XiaoZhi SDK)
2
+
3
+ [![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
4
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
5
+ [![PyPI](https://img.shields.io/badge/pypi-xiaozhi--sdk-blue.svg)](https://pypi.org/project/xiaozhi-sdk/)
6
+
7
+ 基于虾哥的 [小智esp32 websocket 通讯协议](https://github.com/78/xiaozhi-esp32/blob/main/docs/websocket.md) 实现的 Python SDK。
8
+
9
+ 一个用于连接和控制小智设备的 Python SDK。支持以下功能:
10
+ - 实时音频通信
11
+ - MCP 工具集成
12
+ - 设备管理与控制
13
+
14
+ ---
15
+
16
+ ## 📦 安装
17
+
18
+ ```bash
19
+ pip install xiaozhi-sdk
20
+ ```
21
+
22
+ ---
23
+
24
+ ## 🚀 快速开始
25
+
26
+ ### 1. 终端使用
27
+
28
+ 最简单的方式是通过终端直接连接设备:
29
+
30
+ #### 查看帮助信息
31
+
32
+ ```bash
33
+ python -m xiaozhi_sdk -h
34
+ ```
35
+
36
+ 输出示例:
37
+ ```text
38
+ positional arguments:
39
+ device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
40
+
41
+ options:
42
+ -h, --help 显示帮助信息并退出
43
+ --url URL 小智服务 websocket 地址
44
+ --ota_url OTA_URL 小智 OTA 地址
45
+ ```
46
+
47
+ #### 连接设备(需要提供 MAC 地址)
48
+
49
+ ```bash
50
+ python -m xiaozhi_sdk 00:22:44:66:88:00
51
+ ```
52
+
53
+ ### 2. 编程使用
54
+ 参考 [examples](examples/) 文件中的示例代码,可以快速开始使用 SDK。
55
+
56
+
57
+ ### 运行测试
58
+
59
+ ```bash
60
+ pytest tests/
61
+ ```
62
+
63
+
64
+ ---
65
+
66
+ ## 🫡 致敬
67
+
68
+ - 🫡 虾哥的 [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) 项目
Binary file
Binary file
@@ -0,0 +1,17 @@
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
+
@@ -0,0 +1,49 @@
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
+ )
@@ -0,0 +1,19 @@
1
+ import os
2
+ import sys
3
+ import uuid
4
+
5
+ import pytest
6
+
7
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
8
+
9
+ from xiaozhi_sdk.iot import OtaDevice
10
+
11
+
12
+ @pytest.mark.asyncio
13
+ async def test_main():
14
+ serial_number = ""
15
+ license_key = ""
16
+ mac_address = "00:22:44:66:88:00"
17
+ ota = OtaDevice(mac_addr=mac_address, client_id=str(uuid.uuid4()), serial_number=serial_number)
18
+ res = await ota.activate_device()
19
+ await ota.check_activate(res["activation"]["challenge"], license_key)
@@ -0,0 +1,9 @@
1
+ import os
2
+ import sys
3
+
4
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
5
+
6
+ from xiaozhi_sdk.utils import setup_opus
7
+
8
+ setup_opus()
9
+ import opuslib
@@ -0,0 +1,47 @@
1
+ import asyncio
2
+
3
+ import aiohttp
4
+
5
+
6
+ async def explain_image():
7
+ url = "http://api.xiaozhi.me/vision/explain"
8
+ question = "这个图片里有什么?"
9
+ image_path = "./file/image/leijun.jpg"
10
+
11
+ boundary = "----ESP32_CAMERA_BOUNDARY"
12
+ headers = {
13
+ "Content-Type": f"multipart/form-data; boundary={boundary}",
14
+ # 不设置Transfer-Encoding,aiohttp自动处理
15
+ }
16
+
17
+ with open(image_path, "rb") as f:
18
+ img_data = f.read()
19
+
20
+ # 手动构造multipart body
21
+ body = bytearray()
22
+
23
+ # question字段
24
+ body.extend(f"--{boundary}\r\n".encode())
25
+ body.extend(b'Content-Disposition: form-data; name="question"\r\n\r\n')
26
+ body.extend(question.encode("utf-8"))
27
+ body.extend(b"\r\n")
28
+
29
+ # 文件字段头
30
+ body.extend(f"--{boundary}\r\n".encode())
31
+ body.extend(b'Content-Disposition: form-data; name="file"; filename="camera.jpg"\r\n')
32
+ body.extend(b"Content-Type: image/jpeg\r\n\r\n")
33
+ body.extend(img_data)
34
+ body.extend(b"\r\n")
35
+
36
+ # multipart结束
37
+ body.extend(f"--{boundary}--\r\n".encode())
38
+
39
+ async with aiohttp.ClientSession() as session:
40
+ async with session.post(url, data=body, headers=headers) as resp:
41
+ print(f"Status: {resp.status}")
42
+ text = await resp.text()
43
+ print("Response:", text)
44
+
45
+
46
+ if __name__ == "__main__":
47
+ asyncio.run(explain_image())
@@ -0,0 +1,87 @@
1
+ import asyncio
2
+ import os
3
+ import sys
4
+ import time
5
+
6
+ import numpy as np
7
+ import pytest
8
+ import sounddevice as sd
9
+
10
+ sys.path.insert(0, os.path.join(os.path.dirname(__file__), ".."))
11
+
12
+ from xiaozhi_sdk import XiaoZhiWebsocket
13
+ from xiaozhi_sdk.utils import read_audio_file
14
+
15
+
16
+ async def assistant_audio_play(audio_queue, wait_time=1):
17
+ # 创建一个持续播放的流
18
+ stream = sd.OutputStream(samplerate=16000, channels=1, dtype=np.int16)
19
+ stream.start()
20
+ last_time = None
21
+ while True:
22
+ if not audio_queue:
23
+ await asyncio.sleep(0.01)
24
+ if last_time and time.time() - last_time > wait_time:
25
+ break
26
+
27
+ continue
28
+
29
+ pcm_data = audio_queue.popleft()
30
+ stream.write(pcm_data)
31
+ last_time = time.time()
32
+
33
+ stream.stop()
34
+ stream.close()
35
+
36
+
37
+ def mcp_tool_func():
38
+ def mcp_take_photo(data) -> tuple[bytes, bool]:
39
+ with open("./file/image/leijun.jpg", "rb") as f:
40
+ return f.read(), False
41
+
42
+ def mcp_get_device_status(data) -> tuple[dict, bool]:
43
+ data = {
44
+ "audio_speaker": {"volume": 80},
45
+ "screen": {"brightness": 75, "theme": "light"},
46
+ "network": {"type": "wifi", "ssid": "wifi名称", "signal": "strong"},
47
+ }
48
+ return data, False
49
+
50
+ def mcp_set_volume(data) -> tuple[dict, bool]:
51
+ return {}, False
52
+
53
+ return {
54
+ "set_volume": mcp_set_volume,
55
+ "get_device_status": mcp_get_device_status,
56
+ "take_photo": mcp_take_photo,
57
+ }
58
+
59
+
60
+ async def message_handler_callback(message):
61
+ print("message received:", message)
62
+ pass
63
+
64
+
65
+ MAC_ADDR = "00:22:44:66:88:00"
66
+ URL = None
67
+
68
+
69
+ @pytest.mark.asyncio
70
+ async def test_main():
71
+ xiaozhi = XiaoZhiWebsocket(message_handler_callback, url=URL)
72
+ await xiaozhi.set_mcp_tool_callback(mcp_tool_func())
73
+ await xiaozhi.init_connection(MAC_ADDR)
74
+
75
+ # say hellow
76
+ for pcm in read_audio_file("./file/audio/say_hello.wav"):
77
+ await xiaozhi.send_audio(pcm)
78
+ await xiaozhi.send_silence_audio()
79
+ await assistant_audio_play(xiaozhi.output_audio_queue)
80
+
81
+ # say take photo
82
+ for pcm in read_audio_file("./file/audio/take_photo.wav"):
83
+ await xiaozhi.send_audio(pcm)
84
+ await xiaozhi.send_silence_audio()
85
+ await assistant_audio_play(xiaozhi.output_audio_queue, 5)
86
+
87
+ await xiaozhi.close()
@@ -0,0 +1,200 @@
1
+ import asyncio
2
+ import json
3
+ import os
4
+ import re
5
+ import uuid
6
+ from collections import deque
7
+ from typing import Any, Callable, Dict, Optional
8
+
9
+ import websockets
10
+
11
+ from xiaozhi_sdk.config import INPUT_SERVER_AUDIO_SAMPLE_RATE
12
+ from xiaozhi_sdk.iot import OtaDevice
13
+ from xiaozhi_sdk.mcp import McpTool
14
+ from xiaozhi_sdk.utils import get_wav_info, read_audio_file, setup_opus
15
+
16
+ setup_opus()
17
+ from xiaozhi_sdk.opus import AudioOpus
18
+
19
+
20
+ class XiaoZhiWebsocket(McpTool):
21
+
22
+ def __init__(
23
+ self,
24
+ message_handler_callback: Optional[Callable] = None,
25
+ url: Optional[str] = None,
26
+ ota_url: Optional[str] = None,
27
+ audio_sample_rate: int = 16000,
28
+ audio_channels: int = 1,
29
+ ):
30
+ super().__init__()
31
+ self.url = url
32
+ self.ota_url = ota_url
33
+ self.audio_channels = audio_channels
34
+ self.audio_opus = AudioOpus(audio_sample_rate, audio_channels)
35
+
36
+ # 客户端标识
37
+ self.client_id = str(uuid.uuid4())
38
+ self.mac_addr: Optional[str] = None
39
+
40
+ # 回调函数
41
+ self.message_handler_callback = message_handler_callback
42
+
43
+ # 连接状态
44
+ self.hello_received = asyncio.Event()
45
+ self.session_id = ""
46
+ self.websocket = None
47
+ self.message_handler_task: Optional[asyncio.Task] = None
48
+
49
+ # 输出音频
50
+ self.output_audio_queue: deque[bytes] = deque()
51
+
52
+ # OTA设备
53
+ self.ota: Optional[OtaDevice] = None
54
+
55
+ async def _send_hello(self, aec: bool) -> None:
56
+ """发送hello消息"""
57
+ hello_message = {
58
+ "type": "hello",
59
+ "version": 1,
60
+ "features": {"aec": aec, "mcp": True},
61
+ "transport": "websocket",
62
+ "audio_params": {
63
+ "format": "opus",
64
+ "sample_rate": INPUT_SERVER_AUDIO_SAMPLE_RATE,
65
+ "channels": 1,
66
+ "frame_duration": 60,
67
+ },
68
+ }
69
+ await self.websocket.send(json.dumps(hello_message))
70
+ await asyncio.wait_for(self.hello_received.wait(), timeout=10.0)
71
+
72
+ async def _start_listen(self) -> None:
73
+ """开始监听"""
74
+
75
+ listen_message = {"session_id": self.session_id, "type": "listen", "state": "start", "mode": "realtime"}
76
+ await self.websocket.send(json.dumps(listen_message))
77
+
78
+ async def _activate_iot_device(self, ota_info: Dict[str, Any]) -> None:
79
+ """激活IoT设备"""
80
+ if not ota_info.get("activation"):
81
+ return
82
+
83
+ if not self.ota:
84
+ return
85
+
86
+ await self._send_demo_audio()
87
+ challenge = ota_info["activation"]["challenge"]
88
+ await asyncio.sleep(3)
89
+
90
+ for _ in range(10):
91
+ if await self.ota.check_activate(challenge):
92
+ break
93
+ await asyncio.sleep(3)
94
+
95
+ async def _send_demo_audio(self) -> None:
96
+ """发送演示音频"""
97
+ current_dir = os.path.dirname(os.path.abspath(__file__))
98
+ wav_path = os.path.join(current_dir, "../file/audio/greet.wav")
99
+ framerate, channels = get_wav_info(wav_path)
100
+ audio_opus = AudioOpus(framerate, channels)
101
+
102
+ for pcm_data in read_audio_file(wav_path):
103
+ opus_data = await audio_opus.pcm_to_opus(pcm_data)
104
+ await self.websocket.send(opus_data)
105
+ await self.send_silence_audio()
106
+
107
+ async def send_silence_audio(self, duration_seconds: float = 1.2) -> None:
108
+ """发送静音音频"""
109
+ frames_count = int(duration_seconds * 1000 / 60)
110
+ pcm_frame = b"\x00\x00" * int(INPUT_SERVER_AUDIO_SAMPLE_RATE / 1000 * 60)
111
+
112
+ for _ in range(frames_count):
113
+ await self.send_audio(pcm_frame)
114
+
115
+ async def _handle_websocket_message(self, message: Any) -> None:
116
+ """处理接受到的WebSocket消息"""
117
+ if isinstance(message, bytes):
118
+ pcm_array = await self.audio_opus.opus_to_pcm(message)
119
+ self.output_audio_queue.extend(pcm_array)
120
+ else:
121
+ data = json.loads(message)
122
+ message_type = data["type"]
123
+
124
+ if message_type == "hello":
125
+ self.hello_received.set()
126
+ self.session_id = data["session_id"]
127
+ elif message_type == "mcp":
128
+ await self.mcp(data)
129
+ elif self.message_handler_callback:
130
+ await self.message_handler_callback(data)
131
+
132
+ async def _message_handler(self) -> None:
133
+ """消息处理器"""
134
+ try:
135
+ async for message in self.websocket:
136
+ await self._handle_websocket_message(message)
137
+ except websockets.ConnectionClosed:
138
+ if self.message_handler_callback:
139
+ await self.message_handler_callback(
140
+ {"type": "websocket", "state": "close", "source": "sdk.message_handler"}
141
+ )
142
+
143
+ async def set_mcp_tool_callback(self, tool_func: Dict[str, Callable[..., Any]]) -> None:
144
+ """设置MCP工具回调函数"""
145
+ self.tool_func = tool_func
146
+
147
+ async def init_connection(self, mac_addr: str, aec: bool = False) -> None:
148
+ """初始化连接"""
149
+ # 校验MAC地址格式 XX:XX:XX:XX:XX:XX
150
+ mac_pattern = r"^([0-9A-Fa-f]{2}:){5}[0-9A-Fa-f]{2}$"
151
+ if not re.match(mac_pattern, mac_addr):
152
+ raise ValueError(f"无效的MAC地址格式: {mac_addr}。正确格式应为 XX:XX:XX:XX:XX:XX")
153
+
154
+ self.mac_addr = mac_addr.lower()
155
+ self.ota = OtaDevice(self.mac_addr, self.client_id, self.ota_url)
156
+ ota_info = await self.ota.activate_device()
157
+
158
+ headers = {
159
+ "Authorization": "Bearer {}".format(ota_info["websocket"]["token"]),
160
+ "Protocol-Version": "1",
161
+ "Device-Id": self.mac_addr,
162
+ "Client-Id": self.client_id,
163
+ }
164
+ self.url = self.url or ota_info["websocket"]["url"]
165
+ self.websocket = await websockets.connect(uri=self.url, additional_headers=headers)
166
+ self.message_handler_task = asyncio.create_task(self._message_handler())
167
+
168
+ await self._send_hello(aec)
169
+ await self._start_listen()
170
+ asyncio.create_task(self._activate_iot_device(ota_info))
171
+ await asyncio.sleep(0.5)
172
+
173
+ async def send_audio(self, pcm: bytes) -> None:
174
+ """发送音频数据"""
175
+ if not self.websocket:
176
+ return
177
+
178
+ state = self.websocket.state
179
+ if state == websockets.protocol.State.OPEN:
180
+ opus_data = await self.audio_opus.pcm_to_opus(pcm)
181
+ await self.websocket.send(opus_data)
182
+ elif state in [websockets.protocol.State.CLOSED, websockets.protocol.State.CLOSING]:
183
+ if self.message_handler_callback:
184
+ await self.message_handler_callback({"type": "websocket", "state": "close", "source": "sdk.send_audio"})
185
+ self.websocket = None
186
+ await asyncio.sleep(0.5)
187
+ else:
188
+ await asyncio.sleep(0.1)
189
+
190
+ async def close(self) -> None:
191
+ """关闭连接"""
192
+ if self.message_handler_task and not self.message_handler_task.done():
193
+ self.message_handler_task.cancel()
194
+ try:
195
+ await self.message_handler_task
196
+ except asyncio.CancelledError:
197
+ pass
198
+
199
+ if self.websocket:
200
+ await self.websocket.close()
@@ -0,0 +1,101 @@
1
+ import argparse
2
+ import asyncio
3
+ import logging
4
+ import time
5
+ from collections import deque
6
+ from typing import Optional
7
+
8
+ import numpy as np
9
+ import sounddevice as sd
10
+
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
+ logger = logging.getLogger("xiaozhi_sdk")
19
+
20
+ # 全局状态
21
+ input_audio_buffer: deque[bytes] = deque()
22
+ is_playing_audio = False
23
+
24
+
25
+ async def handle_message(message):
26
+ """处理接收到的消息"""
27
+ logger.info("message received: %s", message)
28
+
29
+
30
+ async def play_assistant_audio(audio_queue: deque[bytes]):
31
+ """播放音频流"""
32
+ global is_playing_audio
33
+
34
+ stream = sd.OutputStream(samplerate=INPUT_SERVER_AUDIO_SAMPLE_RATE, channels=1, dtype=np.int16)
35
+ stream.start()
36
+ last_audio_time = None
37
+
38
+ while True:
39
+ if not audio_queue:
40
+ await asyncio.sleep(0.01)
41
+ if last_audio_time and time.time() - last_audio_time > 1:
42
+ is_playing_audio = False
43
+ continue
44
+
45
+ is_playing_audio = True
46
+ pcm_data = audio_queue.popleft()
47
+ stream.write(pcm_data)
48
+ last_audio_time = time.time()
49
+
50
+
51
+ class XiaoZhiClient:
52
+ """小智客户端类"""
53
+
54
+ def __init__(self, mac_address: str, url: Optional[str] = None, ota_url: Optional[str] = None):
55
+ self.mac_address = mac_address
56
+ self.xiaozhi: Optional[XiaoZhiWebsocket] = None
57
+ self.url = url
58
+ self.ota_url = ota_url
59
+
60
+ async def start(self):
61
+ """启动客户端连接"""
62
+ self.xiaozhi = XiaoZhiWebsocket(handle_message, url=self.url, ota_url=self.ota_url)
63
+ await self.xiaozhi.init_connection(self.mac_address, aec=False)
64
+ asyncio.create_task(play_assistant_audio(self.xiaozhi.output_audio_queue))
65
+
66
+ def audio_callback(self, indata, frames, time, status):
67
+ """音频输入回调函数"""
68
+ pcm_data = (indata.flatten() * 32767).astype(np.int16).tobytes()
69
+ input_audio_buffer.append(pcm_data)
70
+
71
+ async def process_audio_input(self):
72
+ """处理音频输入"""
73
+ while True:
74
+ if not input_audio_buffer:
75
+ await asyncio.sleep(0.02)
76
+ continue
77
+
78
+ pcm_data = input_audio_buffer.popleft()
79
+ if not is_playing_audio:
80
+ await self.xiaozhi.send_audio(pcm_data)
81
+
82
+
83
+ async def main():
84
+ """主函数"""
85
+ parser = argparse.ArgumentParser(description="小智SDK客户端")
86
+ parser.add_argument("device", help="小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)")
87
+ parser.add_argument("--url", help="小智服务websocket地址")
88
+ parser.add_argument("--ota_url", help="小智OTA地址")
89
+
90
+ args = parser.parse_args()
91
+ logger.info("Recording... Press Ctrl+C to stop.")
92
+
93
+ client = XiaoZhiClient(args.device, args.url, args.ota_url)
94
+ await client.start()
95
+
96
+ with sd.InputStream(callback=client.audio_callback, channels=1, samplerate=16000, blocksize=960):
97
+ await client.process_audio_input()
98
+
99
+
100
+ if __name__ == "__main__":
101
+ asyncio.run(main())
@@ -0,0 +1,3 @@
1
+ INPUT_SERVER_AUDIO_SAMPLE_RATE = 16000
2
+
3
+ OTA_URL = "https://api.tenclass.net/xiaozhi/ota/"
@@ -0,0 +1,60 @@
1
+ from typing import Any, Dict, List
2
+
3
+ mcp_initialize_payload: Dict[str, Any] = {
4
+ "jsonrpc": "2.0",
5
+ "id": 1,
6
+ "result": {
7
+ "protocolVersion": "2024-11-05",
8
+ "capabilities": {"tools": {}},
9
+ "serverInfo": {"name": "", "version": "0.0.1"},
10
+ },
11
+ }
12
+
13
+ mcp_tool_conf: Dict[str, Dict[str, Any]] = {
14
+ "get_device_status": {
15
+ "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
+ "inputSchema": {"type": "object", "properties": {}},
17
+ },
18
+ "set_volume": {
19
+ "description": "Set the volume of the audio speaker. If the current volume is unknown, you must call `self.get_device_status` tool first and then call this tool.",
20
+ "inputSchema": {
21
+ "type": "object",
22
+ "properties": {"volume": {"type": "integer", "minimum": 0, "maximum": 100}},
23
+ "required": ["volume"],
24
+ },
25
+ },
26
+ "set_brightness": {
27
+ "description": "Set the brightness of the screen.",
28
+ "inputSchema": {
29
+ "type": "object",
30
+ "properties": {"brightness": {"type": "integer", "minimum": 0, "maximum": 100}},
31
+ "required": ["brightness"],
32
+ },
33
+ },
34
+ "set_theme": {
35
+ "description": "Set the theme of the screen. The theme can be `light` or `dark`.",
36
+ "inputSchema": {"type": "object", "properties": {"theme": {"type": "string"}}, "required": ["theme"]},
37
+ },
38
+ "take_photo": {
39
+ "description": "Take a photo and explain it. Use this tool after the user asks you to see something.\nArgs:\n `question`: The question that you want to ask about the photo.\nReturn:\n A JSON object that provides the photo information.",
40
+ "inputSchema": {
41
+ "type": "object",
42
+ "properties": {"question": {"type": "string"}},
43
+ "required": ["question"],
44
+ },
45
+ },
46
+ "open_tab": {
47
+ "description": "Open a web page in the browser. 小智后台:https://xiaozhi.me",
48
+ "inputSchema": {
49
+ "type": "object",
50
+ "properties": {"url": {"type": "string"}},
51
+ "required": ["url"],
52
+ },
53
+ },
54
+ }
55
+
56
+ mcp_tools_payload: Dict[str, Any] = {
57
+ "jsonrpc": "2.0",
58
+ "id": 2,
59
+ "result": {"tools": []},
60
+ }
@@ -0,0 +1,76 @@
1
+ import hashlib
2
+ import hmac
3
+ import json
4
+ from typing import Any, Dict, Optional
5
+
6
+ import aiohttp
7
+
8
+ from xiaozhi_sdk.config import OTA_URL
9
+
10
+ # 常量定义
11
+ DEFAULT_APPLICATION_VERSION = "1.0.0"
12
+ BOARD_TYPE = "xiaozhi-sdk-box"
13
+ USER_AGENT = "xiaozhi-sdk/{}".format(DEFAULT_APPLICATION_VERSION)
14
+ BOARD_NAME = "xiaozhi-sdk-{}".format(DEFAULT_APPLICATION_VERSION)
15
+
16
+
17
+ class OtaDevice:
18
+ """
19
+ OTA设备管理类
20
+
21
+ 用于处理设备的激活和挑战验证操作。
22
+
23
+ Attributes:
24
+ ota_url (str): OTA服务器URL
25
+ mac_addr (str): 设备MAC地址
26
+ client_id (str): 客户端ID
27
+ serial_number (str): 设备序列号
28
+ """
29
+
30
+ def __init__(self, mac_addr: str, client_id: str, ota_url: Optional[str] = None, serial_number: str = "") -> None:
31
+ self.ota_url = ota_url or OTA_URL
32
+ self.mac_addr = mac_addr
33
+ self.client_id = client_id
34
+ self.serial_number = serial_number
35
+
36
+ def _get_base_headers(self) -> Dict[str, str]:
37
+ return {
38
+ "user-agent": USER_AGENT,
39
+ "Device-Id": self.mac_addr,
40
+ "Client-Id": self.client_id,
41
+ "Content-Type": "application/json",
42
+ }
43
+
44
+ async def activate_device(self) -> Dict[str, Any]:
45
+ headers = self._get_base_headers()
46
+ headers["serial-number"] = self.serial_number
47
+
48
+ payload = {
49
+ "application": {"version": DEFAULT_APPLICATION_VERSION},
50
+ "board": {
51
+ "type": BOARD_TYPE,
52
+ "name": BOARD_NAME,
53
+ },
54
+ }
55
+
56
+ async with aiohttp.ClientSession() as session:
57
+ async with session.post(self.ota_url, headers=headers, data=json.dumps(payload)) as response:
58
+ response.raise_for_status()
59
+ return await response.json()
60
+
61
+ async def check_activate(self, challenge: str, license_key: str = "") -> bool:
62
+ url = f"{self.ota_url}/activate"
63
+ headers = self._get_base_headers()
64
+
65
+ hmac_instance = hmac.new(license_key.encode(), challenge.encode(), hashlib.sha256)
66
+ hmac_result = hmac_instance.hexdigest()
67
+
68
+ payload = {
69
+ "serial_number": self.serial_number,
70
+ "challenge": challenge,
71
+ "hmac": hmac_result
72
+ }
73
+
74
+ async with aiohttp.ClientSession() as session:
75
+ async with session.post(url, headers=headers, data=json.dumps(payload)) as response:
76
+ return response.status == 200
@@ -0,0 +1,100 @@
1
+ import json
2
+ import logging
3
+
4
+ import requests
5
+
6
+ from xiaozhi_sdk.data import mcp_initialize_payload, mcp_tool_conf, mcp_tools_payload
7
+
8
+ logger = logging.getLogger("xiaozhi_sdk")
9
+
10
+
11
+ class McpTool(object):
12
+
13
+ def __init__(self):
14
+ self.session_id = ""
15
+ self.explain_url = ""
16
+ self.explain_token = ""
17
+ self.websocket = None
18
+ self.tool_func = {}
19
+
20
+ def get_mcp_json(self, payload: dict):
21
+ return json.dumps({"session_id": self.session_id, "type": "mcp", "payload": payload})
22
+
23
+ def _build_response(self, request_id: str, content: str, is_error: bool = False):
24
+ return self.get_mcp_json(
25
+ {
26
+ "jsonrpc": "2.0",
27
+ "id": request_id,
28
+ "result": {
29
+ "content": [{"type": "text", "text": content}],
30
+ "isError": is_error,
31
+ },
32
+ }
33
+ )
34
+
35
+ async def analyze_image(self, img_byte: bytes, question: str = "这张图片里有什么?"):
36
+ headers = {"Authorization": f"Bearer {self.explain_token}"}
37
+ files = {"file": ("camera.jpg", img_byte, "image/jpeg")}
38
+ payload = {"question": question}
39
+ try:
40
+ response = requests.post(self.explain_url, files=files, data=payload, headers=headers, timeout=5)
41
+ res_json = response.json()
42
+ except Exception as e:
43
+ return "网络异常", True
44
+ if res_json.get("error"):
45
+ return res_json, True
46
+ return res_json, False
47
+
48
+ async def mcp_tool_call(self, mcp_json: dict):
49
+ tool_name = mcp_json["params"]["name"]
50
+ tool_func = self.tool_func[tool_name]
51
+ try:
52
+ tool_res, is_error = tool_func(mcp_json["params"]["arguments"])
53
+ except Exception as e:
54
+ logger.error("tool_func error: %s", e)
55
+ return
56
+
57
+ if tool_name == "take_photo":
58
+ tool_res, is_error = await self.analyze_image(tool_res, mcp_json["params"]["arguments"]["question"])
59
+
60
+ content = json.dumps(tool_res, ensure_ascii=False)
61
+ return self._build_response(mcp_json["id"], content, is_error)
62
+
63
+ async def mcp(self, data: dict):
64
+ payload = data["payload"]
65
+ method = payload["method"]
66
+
67
+ if method == "initialize":
68
+ self.explain_url = payload["params"]["capabilities"]["vision"]["url"]
69
+ self.explain_token = payload["params"]["capabilities"]["vision"]["token"]
70
+
71
+ mcp_initialize_payload["id"] = payload["id"]
72
+ await self.websocket.send(self.get_mcp_json(mcp_initialize_payload))
73
+
74
+ elif method == "notifications/initialized":
75
+ # print("\nMCP 工具初始化")
76
+ pass
77
+
78
+ elif method == "notifications/cancelled":
79
+ logger.error("MCP 工具加载失败")
80
+
81
+ elif method == "tools/list":
82
+ mcp_tools_payload["id"] = payload["id"]
83
+ tool_list = []
84
+ for name, func in self.tool_func.items():
85
+ if func:
86
+ tool_list.append(name)
87
+ mcp_tool_conf[name]["name"] = name
88
+ mcp_tools_payload["result"]["tools"].append(mcp_tool_conf[name])
89
+ await self.websocket.send(self.get_mcp_json(mcp_tools_payload))
90
+ if tool_list:
91
+ logger.info("MCP 加载成功,当前可用工具列表为:%s", tool_list)
92
+
93
+ elif method == "tools/call":
94
+ tool_name = payload["params"]["name"]
95
+ if not self.tool_func.get(tool_name):
96
+ raise Exception("Tool not found")
97
+ mcp_res = await self.mcp_tool_call(payload)
98
+ await self.websocket.send(mcp_res)
99
+ else:
100
+ logger.warning("unknown method %s: %s", method, payload)
@@ -0,0 +1,55 @@
1
+ import av
2
+ import numpy as np
3
+ import opuslib
4
+
5
+ from xiaozhi_sdk import INPUT_SERVER_AUDIO_SAMPLE_RATE
6
+
7
+
8
+ class AudioOpus:
9
+
10
+ def __init__(self, sample_rate, channels):
11
+ self.sample_rate = sample_rate
12
+ self.channels = channels
13
+
14
+ # 创建 Opus 编码器
15
+ self.opus_encoder = opuslib.Encoder(
16
+ fs=sample_rate, channels=channels, application=opuslib.APPLICATION_VOIP # 采样率 # 单声道 # 语音应用
17
+ )
18
+
19
+ # 创建 Opus 解码器
20
+ self.opus_decoder = opuslib.Decoder(
21
+ fs=INPUT_SERVER_AUDIO_SAMPLE_RATE, # 采样率
22
+ channels=1, # 单声道
23
+ )
24
+
25
+ self.resampler = av.AudioResampler(format="s16", layout="mono", rate=sample_rate)
26
+
27
+ async def pcm_to_opus(self, pcm):
28
+ pcm_array = np.frombuffer(pcm, dtype=np.int16)
29
+ pcm_bytes = pcm_array.tobytes()
30
+ return self.opus_encoder.encode(pcm_bytes, 960)
31
+
32
+ async def change_sample_rate(self, pcm_array):
33
+ if self.sample_rate == INPUT_SERVER_AUDIO_SAMPLE_RATE:
34
+ return pcm_array.reshape(1, 960)
35
+
36
+ c = int(self.sample_rate / INPUT_SERVER_AUDIO_SAMPLE_RATE)
37
+ frame = av.AudioFrame.from_ndarray(np.array(pcm_array).reshape(1, -1), format="s16", layout="mono")
38
+ frame.sample_rate = INPUT_SERVER_AUDIO_SAMPLE_RATE # Assuming input is 16kHz
39
+ resampled_frames = self.resampler.resample(frame)
40
+ samples = resampled_frames[0].to_ndarray().flatten()
41
+ new_frame = av.AudioFrame.from_ndarray(
42
+ samples.reshape(1, -1),
43
+ format="s16",
44
+ layout="mono",
45
+ )
46
+ new_frame.sample_rate = self.sample_rate
47
+ new_samples = new_frame.to_ndarray().flatten()
48
+ arr_padded = np.pad(new_samples, (0, 960 * c - new_samples.shape[0]), mode="constant", constant_values=0)
49
+ return arr_padded.reshape(c, 960)
50
+
51
+ async def opus_to_pcm(self, opus):
52
+ pcm_data = self.opus_decoder.decode(opus, 960)
53
+ pcm_array = np.frombuffer(pcm_data, dtype=np.int16)
54
+ samples = await self.change_sample_rate(pcm_array)
55
+ return samples
@@ -0,0 +1,57 @@
1
+ import ctypes.util
2
+ import os
3
+ import platform
4
+ import wave
5
+
6
+
7
+ def get_wav_info(file_path):
8
+ with wave.open(file_path, "rb") as wav_file:
9
+ return wav_file.getframerate(), wav_file.getnchannels()
10
+
11
+
12
+ def read_audio_file(file_path):
13
+ """
14
+ 读取音频文件并通过yield返回PCM流
15
+
16
+ Args:
17
+ file_path (str): 音频文件路径
18
+
19
+ Yields:
20
+ bytes: PCM音频数据块
21
+ """
22
+ with wave.open(file_path, "rb") as wav_file:
23
+ while True:
24
+ pcm = wav_file.readframes(960) # 每次读取960帧(60ms的音频数据)
25
+ if not pcm:
26
+ break
27
+ yield pcm
28
+
29
+
30
+ def setup_opus():
31
+
32
+ def fake_find_library(name):
33
+ current_dir = os.path.dirname(os.path.abspath(__file__))
34
+ if name == "opus":
35
+ system = platform.system().lower()
36
+ machine = platform.machine().lower()
37
+
38
+ # 检测架构
39
+ if machine in ["x86_64", "amd64", "x64"]:
40
+ arch = "x64"
41
+ elif machine in ["arm64", "aarch64"]:
42
+ arch = "arm64"
43
+ else:
44
+ # 默认使用x64作为回退
45
+ arch = "x64"
46
+
47
+ if system == "darwin": # macOS
48
+ return f"{current_dir}/../file/opus/macos-{arch}-libopus.dylib"
49
+ elif system == "windows": # Windows
50
+ return f"{current_dir}/../file/opus/windows-{arch}-opus.dll"
51
+ elif system == "linux": # Linux
52
+ return f"{current_dir}/../file/opus/linux-{arch}-libopus.so"
53
+ else:
54
+ # 默认情况,尝试系统查找
55
+ return ctypes.util.find_library(name)
56
+
57
+ ctypes.util.find_library = fake_find_library
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: xiaozhi-sdk
3
+ Version: 0.0.2
4
+ Summary: 一个用于连接和控制小智智能设备的Python SDK,支持实时音频通信、MCP工具集成和设备管理功能。
5
+ Home-page: https://github.com/dairoot/xiaozhi-sdk
6
+ Author: dairoot
7
+ Author-email: 623815825@qq.com
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: License :: OSI Approved :: MIT License
10
+ Classifier: Operating System :: OS Independent
11
+ Requires-Python: >=3.8
12
+ Description-Content-Type: text/markdown
13
+ License-File: LICENSE
14
+ Requires-Dist: numpy
15
+ Requires-Dist: websockets
16
+ Requires-Dist: aiohttp
17
+ Requires-Dist: av
18
+ Requires-Dist: opuslib
19
+ Requires-Dist: requests
20
+ Requires-Dist: sounddevice
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
+ Dynamic: license-file
29
+ Dynamic: requires-dist
30
+ Dynamic: requires-python
31
+ Dynamic: summary
32
+
33
+ # 小智SDK (XiaoZhi SDK)
34
+
35
+ [![Python Version](https://img.shields.io/badge/python-3.8+-blue.svg)](https://www.python.org/downloads/)
36
+ [![License](https://img.shields.io/badge/license-MIT-green.svg)](LICENSE)
37
+ [![PyPI](https://img.shields.io/badge/pypi-xiaozhi--sdk-blue.svg)](https://pypi.org/project/xiaozhi-sdk/)
38
+
39
+ 基于虾哥的 [小智esp32 websocket 通讯协议](https://github.com/78/xiaozhi-esp32/blob/main/docs/websocket.md) 实现的 Python SDK。
40
+
41
+ 一个用于连接和控制小智设备的 Python SDK。支持以下功能:
42
+ - 实时音频通信
43
+ - MCP 工具集成
44
+ - 设备管理与控制
45
+
46
+ ---
47
+
48
+ ## 📦 安装
49
+
50
+ ```bash
51
+ pip install xiaozhi-sdk
52
+ ```
53
+
54
+ ---
55
+
56
+ ## 🚀 快速开始
57
+
58
+ ### 1. 终端使用
59
+
60
+ 最简单的方式是通过终端直接连接设备:
61
+
62
+ #### 查看帮助信息
63
+
64
+ ```bash
65
+ python -m xiaozhi_sdk -h
66
+ ```
67
+
68
+ 输出示例:
69
+ ```text
70
+ positional arguments:
71
+ device 你的小智设备的MAC地址 (格式: XX:XX:XX:XX:XX:XX)
72
+
73
+ options:
74
+ -h, --help 显示帮助信息并退出
75
+ --url URL 小智服务 websocket 地址
76
+ --ota_url OTA_URL 小智 OTA 地址
77
+ ```
78
+
79
+ #### 连接设备(需要提供 MAC 地址)
80
+
81
+ ```bash
82
+ python -m xiaozhi_sdk 00:22:44:66:88:00
83
+ ```
84
+
85
+ ### 2. 编程使用
86
+ 参考 [examples](examples/) 文件中的示例代码,可以快速开始使用 SDK。
87
+
88
+
89
+ ### 运行测试
90
+
91
+ ```bash
92
+ pytest tests/
93
+ ```
94
+
95
+
96
+ ---
97
+
98
+ ## 🫡 致敬
99
+
100
+ - 🫡 虾哥的 [xiaozhi-esp32](https://github.com/78/xiaozhi-esp32) 项目
@@ -0,0 +1,40 @@
1
+ LICENSE
2
+ MANIFEST.in
3
+ README.md
4
+ setup.cfg
5
+ setup.py
6
+ file/audio/greet.wav
7
+ file/audio/say_hello.wav
8
+ file/audio/take_photo.wav
9
+ file/image/leijun.jpg
10
+ file/opus/linux-arm64-libopus.so
11
+ file/opus/linux-x64-libopus.so
12
+ file/opus/macos-arm64-libopus.dylib
13
+ file/opus/macos-x64-libopus.dylib
14
+ file/opus/windows-x86_64-opus.dll
15
+ tests/test_iot.py
16
+ tests/test_opus.py
17
+ tests/test_pic.py
18
+ tests/test_xiaozhi.py
19
+ xiaozhi_sdk/__init__.py
20
+ xiaozhi_sdk/__main__.py
21
+ xiaozhi_sdk/config.py
22
+ xiaozhi_sdk/data.py
23
+ xiaozhi_sdk/iot.py
24
+ xiaozhi_sdk/mcp.py
25
+ xiaozhi_sdk/opus.py
26
+ xiaozhi_sdk/utils.py
27
+ xiaozhi_sdk.egg-info/PKG-INFO
28
+ xiaozhi_sdk.egg-info/SOURCES.txt
29
+ xiaozhi_sdk.egg-info/dependency_links.txt
30
+ xiaozhi_sdk.egg-info/requires.txt
31
+ xiaozhi_sdk.egg-info/top_level.txt
32
+ xiaozhi_sdk/../file/audio/greet.wav
33
+ xiaozhi_sdk/../file/audio/say_hello.wav
34
+ xiaozhi_sdk/../file/audio/take_photo.wav
35
+ xiaozhi_sdk/../file/image/leijun.jpg
36
+ xiaozhi_sdk/../file/opus/linux-arm64-libopus.so
37
+ xiaozhi_sdk/../file/opus/linux-x64-libopus.so
38
+ xiaozhi_sdk/../file/opus/macos-arm64-libopus.dylib
39
+ xiaozhi_sdk/../file/opus/macos-x64-libopus.dylib
40
+ xiaozhi_sdk/../file/opus/windows-x86_64-opus.dll
@@ -0,0 +1,8 @@
1
+ numpy
2
+ websockets
3
+ aiohttp
4
+ av
5
+ opuslib
6
+ requests
7
+ sounddevice
8
+ python-socks
@@ -0,0 +1 @@
1
+ xiaozhi_sdk