xiaozhi-sdk 0.0.6__tar.gz → 0.0.7__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of xiaozhi-sdk might be problematic. Click here for more details.

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