Author SHA1 Message Date
Del Wang 58acbc29f4 feat(xiaozhi): 新增小智 AI 一键运行 Docker 镜像 2025-05-19 00:16:49 +08:00
Del Wang 59c3280938 feat(xiaozhi): 支持在 python 端监听 event 和 runShell 2025-05-11 20:47:02 +08:00
Del Wang e687280ab1 fix: 修复自启动脚本 2025-05-11 19:13:21 +08:00
Del Wang 6bdd1b9683 docs: 更新绑定设备后需要重启生效的说明 2025-05-11 16:04:27 +08:00
Del Wang c8e7861bdb fix: ota version 2025-05-11 15:23:23 +08:00
Del Wang 8fc5f073b5 docs: 更新 xiaozhi-esp32-server 连接教程 2025-05-11 15:16:22 +08:00
Del Wang 6eca122f24 chore(xiaozhi): 更新配置文件 2025-05-11 15:12:15 +08:00
Del Wang 9a8e07a68e docs: update readme 2025-05-11 11:46:12 +08:00
Del Wang cde047af19 feat(migpt): 新增 Docker 一键运行镜像 2025-05-11 11:34:42 +08:00
Del Wang a73ef08074 fix: echo server address 2025-05-10 17:15:40 +08:00
Del Wang cf853bb07b docs(kws): 更新适配机型的相关说明 2025-05-10 13:22:17 +08:00
Del Wang 9129afcc99 feat: 新增 kws 调试脚本 2025-05-10 12:55:19 +08:00
Del Wang a2c9cd5b70 docs: 更新启动脚本开机自启动相关说明 2025-05-10 02:26:36 +08:00
Del Wang b4f52ee5b9 feat: 新增 client 一键安装脚本 2025-05-10 01:41:58 +08:00
Del Wang 73bf1c6f4d docs: 更新小爱音箱自定义唤醒词演示视频 2025-05-10 01:05:32 +08:00
Del Wang e39675abb8 docs: 更新唤醒词识别常见问题说明 2025-05-09 23:39:43 +08:00
Del Wang 95caae8aa4 feat: 完成小爱音箱自定义唤醒词 2025-05-08 23:31:01 +08:00
Del Wang 8d5faa28dc fix: kill 2025-05-08 23:15:44 +08:00
Del Wang f8d4599cc4 fix: kws monitor 2025-05-08 23:13:41 +08:00
Del Wang e9e4ca92dd fix: kill kws 2025-05-08 23:08:44 +08:00
Del Wang 3f85e07ba7 fix: 确保 kws 正确启动 2025-05-08 23:07:07 +08:00
Del Wang ca728e3dfc feat: 新增 client-kws 2025-05-08 22:38:30 +08:00
Del Wang fc9409e55f chore: ignore temp dir 2025-05-08 00:59:54 +08:00
Del Wang 3aa0cf673b chore: 更新默认唤醒词 2025-05-08 00:50:55 +08:00
Del Wang e3732a7f1c chore: kws 测试 2025-05-08 00:24:55 +08:00
Del Wang 62c7c88eab feat: 新增 KwsMonitor 2025-05-07 23:08:32 +08:00
Del Wang 20c21ca609 feat: 添加自定义唤醒词相关教程 2025-05-06 23:33:11 +08:00
Del Wang a74d92b4b6 docs: 添加 SSH 链接异常的相关说明 2025-05-06 21:12:13 +08:00
Del Wang 4a0fde4724 docs: 添加连接不上设备的相关说明 2025-05-06 21:03:09 +08:00
Del Wang 755ba2f202 chore: /data/open-xiaoai 2025-05-06 20:54:57 +08:00
Del Wang 767b7c4548 docs: 添加开机启动脚本的相关说明 2025-05-06 20:34:23 +08:00
Del Wang 2cb5300577 docs: 添加交叉编译相关说明 2025-05-06 20:26:05 +08:00
Del Wang 2ebfb2241a fix: 修复启动脚本未生效的问题 2025-05-06 08:27:18 +08:00
Del Wang c3cac9b2b4 chore: 调整项目演示程序目录结构 2025-04-26 21:46:32 +08:00
Del Wang 39febdfe57 docs: 更新使用说明 2025-04-26 19:11:16 +08:00
Del Wang 012a123fdc fix: 更新 Gemini AI 系统 prompt 2025-04-26 19:07:46 +08:00
Del Wang e53bc65715 feat: 新增 Gemini Live API 接入演示 2025-04-26 19:00:56 +08:00
Del Wang 0900b4cf2a fix: 修复默认录音和播放音频参数 2025-04-26 15:01:49 +08:00
Del Wang 4e7b6f6026 fix: change default sample rate to 16khz 2025-04-26 14:16:48 +08:00
Del Wang 10c464a4e9 fix: 修配置文件 deepMerge 异常的问题 2025-04-21 16:19:03 +08:00
Del Wang 487848d9b0 docs: 更新参数说明 2025-04-19 10:34:38 +08:00
Del Wang 66dc6ef741 docs: 更新刷机成功后重启设备的相关说明 2025-04-19 10:26:40 +08:00
Del Wang b63a8f95a6 docs: 更新 callAIKeywords 参数说明 2025-04-19 10:16:20 +08:00
Del Wang 9ab2ec4058 fix: /usr/sbin/tts_play.sh 2025-04-19 10:12:18 +08:00
Del Wang 1bdea51c3a docs: 刷机成功后需要重启小爱音箱 2025-04-18 23:55:36 +08:00
Del Wang 9506c83223 fix: uname -s 2025-04-18 22:13:03 +08:00
Del Wang 29f4f77c7a feat: 添加 macOS 专用小爱音箱刷机工具 2025-04-14 23:39:18 +08:00
Del Wang b7716e8654 feat: 小爱音箱 Pro v1.88.206 2025-04-14 22:48:41 +08:00
Del Wang 8838087fd7 fix: patch asound.conf.dts 2025-04-14 22:40:27 +08:00
Del Wang 6d04185792 fix: patch asound.conf.dts 2025-04-14 22:37:37 +08:00
Del Wang e0f4dba612 fix: 修复 LX06 声卡补丁 2025-04-14 21:25:28 +08:00
Del Wang 3280809844 docs: 小爱音箱 Pro 声卡补丁有问题,暂时请勿下载/制作 2025-04-14 10:11:57 +08:00
Del Wang 80b5ae4db7 docs: remove UTM 2025-04-13 22:33:35 +08:00
Del Wang 8f85d2dd56 docs: add banner image 2025-04-13 20:03:34 +08:00
Del Wang 89019c7125 docs: 新增相关项目推荐和参考链接 2025-04-13 19:21:37 +08:00
Del Wang a863da3067 feat: 开放小爱音箱 Pro 刷机教程 2025-04-13 18:57:41 +08:00
Del Wang aebf94d5cd docs: 请勿下载使用不明来历的固件! 2025-04-13 13:04:54 +08:00
Del Wang 949dee9500 docs: 更新补丁固件下载注意事项 2025-04-13 13:03:39 +08:00
Del Wang 05072facfd chore: no false 2025-04-13 12:28:48 +08:00
124 changed files with 5933 additions and 1696 deletions
+30
View File
@@ -0,0 +1,30 @@
# 排除全部文件
**/*
# Rust
!packages/client-rust/src
!packages/client-rust/Cargo.toml
!packages/client-rust/Cargo.lock
# MiGPT
!examples/migpt/src
!examples/migpt/migpt
!examples/migpt/config.ts
!examples/migpt/package.json
!examples/migpt/pnpm-lock.yaml
!examples/migpt/Cargo.toml
!examples/migpt/Cargo.lock
!examples/migpt/tsconfig.json
examples/migpt/migpt/open-xiaoai.node
# XiaoZhi
!examples/xiaozhi/src
!examples/xiaozhi/xiaozhi
!examples/xiaozhi/Cargo.toml
!examples/xiaozhi/Cargo.lock
!examples/xiaozhi/config.py
!examples/xiaozhi/main.py
!examples/xiaozhi/uv.lock
!examples/xiaozhi/.python-version
!examples/xiaozhi/requirements.txt
!examples/xiaozhi/pyproject.toml
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
name: 小爱音箱 Pro v${{ steps.build.outputs.version }}
tag: ${{ steps.build.outputs.tag }}
body: 小爱音箱 Pro v${{ steps.build.outputs.version }} 固件更新
draft: true
draft: false
prerelease: false
makeLatest: latest
removeArtifacts: true
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
name: Xiaomi 智能音箱 Pro v${{ steps.build.outputs.version }}
tag: ${{ steps.build.outputs.tag }}
body: Xiaomi 智能音箱 Pro v${{ steps.build.outputs.version }} 固件更新
draft: true
draft: false
prerelease: false
makeLatest: latest
removeArtifacts: true
+2
View File
@@ -14,6 +14,7 @@ node_modules
# Testing
coverage
temp
# Turbo
.turbo
@@ -38,6 +39,7 @@ yarn-error.log*
.venv
.mypy_cache
__pycache__
*.egg-info
# Misc
.DS_Store
+42 -19
View File
@@ -2,6 +2,8 @@
让小爱音箱「听见你的声音」,解锁无限可能。
![](./docs/images/cover.jpg)
## 简介
2017 年,当全球首款千万级销量的智能音箱诞生时,我们以为触摸到了未来。但很快发现,这些设备被困在「指令-响应」的牢笼里:
@@ -10,7 +12,7 @@
- 它能执行命令,却不会主动思考
- 它有千万用户,却只有一套思维
我们曾幻想中的"贾维斯"级人工智能,在现实场景中沦为"闹钟+音乐播放器"。
我们曾幻想中的"贾维斯"级人工智能,在现实场景中沦为"天气预报+音乐播放器"。
**真正的智能不应被预设的代码逻辑所束缚,而应像生命体般在交互中进化。**
@@ -26,41 +28,62 @@
👉 [小爱音箱接入小智 AI 演示视频](https://www.bilibili.com/video/BV1NBXWYSEvX)
[![](./assets/images/xiaozhi.jpg)](https://www.bilibili.com/video/BV1NBXWYSEvX)
[![](./docs/images/xiaozhi.jpg)](https://www.bilibili.com/video/BV1NBXWYSEvX)
👉 [小爱音箱自定义唤醒词演示视频](https://www.bilibili.com/video/BV1YfVUz5EMj)
[![](./docs/images/kws.jpg)](https://www.bilibili.com/video/BV1YfVUz5EMj)
👉 [小爱音箱接入 MiGPT 演示视频](https://www.bilibili.com/video/BV1N1421y7qn)
[![](./assets/images/migpt.jpg)](https://www.bilibili.com/video/BV1N1421y7qn)
[![](./docs/images/migpt.jpg)](https://www.bilibili.com/video/BV1N1421y7qn)
## 刷机教程
## 快速开始
> [!CAUTION]
> 刷机有风险,操作需谨慎。
整理中,敬请期待 :)
## 使用说明
> [!IMPORTANT]
> 本教程仅适用于 **小爱音箱 ProLX06** 和 **Xiaomi 智能音箱 ProOH2P** 这两款机型,**其他型号**的小爱音箱请勿直接使用!🚨
本项目由 Client 端 + Server 端两部分组成,你可以按照以下顺序运行该项目:
1. 按照刷机教程更新小爱音箱固件,开启 SSH 连接到小爱音箱(整理教程中……
2.你的电脑上编译好 Rust 补丁程序(Client 端),然后复制到小爱音箱上运行 👉 [教程](packages/client-rust/README.md)
3. 在你的电脑上运行 Server 端演示程序,体验小爱音箱的全新能力 ✨
- Python Server 演示 👉 [小爱音箱接入小智 AI](packages/server-python/README.md)
- Node.js Server 演示 👉 [小爱音箱接入 MiGPT-Next](packages/server-node/README.md)
1. 刷机更新小爱音箱补丁固件,开启 SSH 连接到小爱音箱 👉 [教程](docs/flash.md)
2.小爱音箱上安装运行 Client 端补丁程序 👉 [教程](packages/client-rust/README.md)
3. 运行以下演示程序,体验小爱音箱的全新能力 ✨
- 👉 [小爱音箱接入小智 AI](examples/xiaozhi/README.md)
- 👉 [小爱音箱自定义唤醒词](examples/kws/README.md)
- 👉 [小爱音箱接入 MiGPT(完美版)](examples/migpt/README.md)
- 👉 [小爱音箱接入 Gemini Live API](examples/gemini/README.md)
以上皆为抛砖引玉,你也可以自己亲手编写想要的功能,一切由你定义!
以上皆为抛砖引玉,你也可以亲手编写自己想要的功能(比如:Airplay、DLNA、开启 DTS 音效等),一切由你定义!
## 相关项目
> [!TIP]
> 技术的意义在于分享与共创。如果你打算或正在用本项目做些有趣的事情,
> 技术的意义在于分享与共创。如果你打算或正在使用本项目做些有趣的事情,
> 欢迎提交 PR 或 issue 分享你的项目和创意。✨
如果你不想刷机,或者不是小爱音箱 Pro,下面的项目或许对你有用:
- https://github.com/idootop/mi-gpt
- https://github.com/idootop/migpt-next
- https://github.com/yihong0618/xiaogpt
- https://github.com/hanxi/xiaomusic
## 参考链接
如果你想要了解更多技术细节,下面的链接可能对你有用:
- https://github.com/yihong0618/gitblog/issues/258
- https://github.com/jialeicui/open-lx01
- https://github.com/duhow/xiaoai-patch
- https://javabin.cn/2021/xiaoai_fm.html
- https://xuanxuanblingbling.github.io/iot/2022/09/16/mi/
## 免责声明
1. **适用范围**
本项目为非盈利开源项目,仅限于技术原理研究、安全漏洞验证及非营利性个人使用。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
本项目为开源非营利项目,仅供学术研究或个人测试用途。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
2. **非官方声明**
本项目由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务的所有权利归属小米集团。若权利方主张权益,使用者应立即主动停止使用并删除本项目。
本项目由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务的所有权利归属小米集团。若权利方主张权益,使用者应立即主动停止使用并删除本项目。
继续下载或运行本项目,即表示您已完整阅读并同意[用户协议](agreement.md),否则请立即终止使用并彻底删除本项目。
+2 -2
View File
@@ -4,11 +4,11 @@
## 授权范围
本项目授权范围仅限于技术原理研究、安全漏洞验证及非营利性个人使用。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
本项目仅供学术研究或个人测试用途。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
## 非官方声明
本项目为非盈利开源项目,由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务等知识产权均归属权利方所有。若权利方主张权益,用户应立即终止使用并彻底销毁项目副本。
本项目为开源非营利项目,由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务等知识产权均归属权利方所有。若权利方主张权益,用户应立即终止使用并彻底销毁项目副本。
## 风险告知
+115
View File
@@ -0,0 +1,115 @@
# 小爱音箱 Pro 刷机教程
> [!IMPORTANT]
> 本教程仅适用于 **小爱音箱 ProLX06** 和 **Xiaomi 智能音箱 ProOH2P** 这两款机型,**其他型号**的小爱音箱请勿直接使用!🚨
### 准备条件
- 小爱音箱
- Xiaomi 智能音箱 ProOH2P
- 小爱音箱 ProLX06
- 数据线
- Type-C(适用于新款小爱音箱 Pro,**无需拆机**)
- Micro USB(旧款小爱音箱 Pro 用这种,**需要拆机**)
> [!NOTE]
> 注意:数据线不能只是充电线,需要连接到电脑上能传输数据才行。
### 连接电脑
新款的 Xiaomi 智能音箱 Pro 可以直接使用 Type-C 数据线通过音响底部的 Type-C 接口连接到电脑。
旧款小爱音箱 Pro 需要先把音箱外壳拆下来([参考步骤](https://www.52audio.com/archives/38303.html)),然后在主板左上角找到 Mico USB 的调试接口,使用 Mico USB 数据线连接到电脑。
![](./images/mico-usb.jpg)
### 开始刷机
> [!CAUTION]
> 刷机有风险,操作需谨慎。刷机可能会造成设备失去保修资格,变砖无法运行等。请自行评估相关风险,一切后果自负!🚨
首先,下载或自行制作[补丁固件](../packages/client-patch/README.md),重命名为 `root_patched.squashfs`
然后,下载刷机工具:https://androidmtk.com/download-amlogic-flash-tool
把下载好的刷机工具文件夹解压到桌面,重命名为 `Amlogic_Flash_Tool_v6.0.0`
> [!TIP]
> 如果你是 macOS 系统,请到此处下载[刷机工具和使用教程](../packages/flash-tool/README.md)。
![](./images/driver.jpg)
然后运行 `AMLLogic driver installer.exe` 文件,按照提示安装 USB 驱动。
![](./images/bash.jpg)
安装好驱动后,打开 `/bin` 目录,然后在空白处右键选择使用 [Git Bash](https://git-scm.com/downloads) 打开当前目录。
> [!NOTE]
> 请确认你已经将小爱音箱用数据线连接到了 Windows 电脑,并且安装好了驱动程序。
然后,请按照说明在打开的终端里依次执行以下命令:
```shell
# 🚗 第 1 步:拔掉小爱音箱的电源线,重新插上电源后,立即在终端执行以下命令
./update.exe identify
# 如果显示类似下面的内容,说明小爱音箱已经成功连接到了电脑,可以继续进行下一步
# This firmware version is 0-7-0-16-0-0-0-0
# 否则继续快速重试几次该命令,或者重新拔掉电源上电后重试,直至显示出来版本号。
# ⏰ 第 2 步:设置启动分区和等待时长
# 设置 uboot 启动等待 15s
./update.exe bulkcmd "setenv bootdelay 15"
# 设置 boot0 作为启动分区
./update.exe bulkcmd "setenv boot_part boot0"
# 保存环境变量,使生效
./update.exe bulkcmd "saveenv"
# ⚡️ 第 3 步:刷入 system0
./update.exe partition system0 root_patched.squashfs
# 注意:需要把 root_patched.squashfs 替换成你下载的补丁固件实际路径
```
> [!TIP]
> 如果你卡在第一步,连接不上设备,可以按照[此教程](https://github.com/idootop/open-xiaoai/issues/6#issuecomment-2815632879)排查问题。
> [!NOTE]
> 提示刷机成功之后,拔掉数据线和电源,重新插电重启小爱音箱即可。
> 如果重启之后小爱音箱没有反应,可以拔掉电源等几分钟再重新上电开机。
> 如果还是没反应,可以重新刷机试试看,或者将启动分区设置成 `boot1` 恢复原系统启动。
### SSH 连接到小爱音箱
刷机成功后,补丁固件默认开启 SSH 功能,默认密码 `open-xiaoai`
```shell
ssh -o HostKeyAlgorithms=+ssh-rsa root@你的小爱音箱局域网IP地址
# 比如:ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.227
```
> [!TIP]
> 如果刷机之后 SSH 连接不上去小爱音箱,请检查:
> - 你设置的启动分区(boot0)是否和你刷写的系统分区(system0)一致(修改启动分区之后,需要 `saveenv` 才会保存生效)
> - 你的电脑和小爱音箱是否处在同一网络环境下(可以更换路由器试试看)
> - 你的内网防火墙是否屏蔽了 22 端口(SSH 默认连接端口)
> - 如果你是自己构建的补丁固件,请检查 SSH 相关的 [patch](../packages/client-patch/patches/01-ssh.patch) 是否生效
### 更新系统
小爱音箱有 2 套系统,上面我们只是将 `system0` 刷成了打补丁后的系统。
如果之后你想要更新系统,或者换回原来的系统,将启动分支设置成 `boot1` 重启即可。
>
```shell
# 连接到 SSH 后执行
fw_env -s boot_part boot1
# 或者使用和上面刷机相同的方式
./update.exe bulkcmd "setenv boot_part boot1"
./update.exe bulkcmd "saveenv"
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Before

Width:  |  Height:  |  Size: 627 KiB

After

Width:  |  Height:  |  Size: 627 KiB

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

@@ -13,7 +13,7 @@ pyo3-async-runtimes = { version = "0.24", features = [
"attributes",
"tokio-runtime",
] }
open-xiaoai = { path = "../client-rust" }
open-xiaoai = { path = "../../packages/client-rust" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
serde = { version = "1.0", features = ["derive"] }
+33
View File
@@ -0,0 +1,33 @@
# Open-XiaoAI x Gemini Live API
小爱音箱接入[Gemini Live API](https://ai.google.dev/gemini-api/docs/live) 的演示程序,支持自动 VAD + 连续对话。
> [!IMPORTANT]
> 你需要先到 [Google AI Studio](https://aistudio.google.com) 注册账号并[创建 API 密钥](https://aistudio.google.com/apikey)。然后更新 `GEMINI_API_KEY` 环境变量,或 `gemini/gemini.py` 文件中的密钥。
## 环境准备
为了能够正常编译运行该项目,你需要安装以下依赖环境/工具:
- uvhttps://github.com/astral-sh/uv
- Rust: https://www.rust-lang.org/learn/get-started
## 编译运行
> [!NOTE]
> 请先确认你已经将小爱音箱刷机成功,并安装运行了 Rust 补丁程序 [👉 教程](../../packages/client-rust/README.md),否则该项目启动后收不到音频输入,将无法正常工作。
```bash
# 安装 Python 依赖
uv sync --locked
# 编译运行
uv run main.py
```
## 注意事项
该演示暂不支持中断 AI 的回复,需要等待 AI 回答完毕后才能重新响应用户的语音输入。
> [!NOTE]
> 本项目只是一个简单的演示程序,抛砖引玉。如果你想要更多的功能,可以借助 Python 丰富的 AI 生态自行实现。
+4
View File
@@ -0,0 +1,4 @@
from .gemini import Gemini
from .xiaoai import XiaoAi
__all__ = ["Gemini", "XiaoAi"]
+104
View File
@@ -0,0 +1,104 @@
import os
from typing import Awaitable, Callable, Optional
from google import genai
from google.genai import types
GEMINI_MODEL = "gemini-2.0-flash-live-001"
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or "你的 API KEY"
class Gemini:
running = False
client = genai.Client(api_key=GEMINI_API_KEY)
session: Optional[genai.live.AsyncSession] = None
config = types.LiveConnectConfig(
response_modalities=[types.Modality.AUDIO],
system_instruction=types.Content(
parts=[types.Part(text="你是小爱音箱,请用中文回答用户的问题。")]
),
speech_config=types.SpeechConfig(
language_code="cmn-CN",
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Leda")
),
),
context_window_compression=(
types.ContextWindowCompressionConfig(
sliding_window=types.SlidingWindow(),
)
),
)
@classmethod
async def send_text(cls, text: str):
if not cls.session:
return
await cls.session.send_client_content(
turns={"role": "user", "parts": [{"text": text}]},
turn_complete=True,
)
@classmethod
async def send_audio(cls, data: bytes):
if not cls.session:
return
await cls.session.send_realtime_input(
audio=types.Blob(data=data, mime_type="audio/pcm;rate=16000")
)
@classmethod
def stop(cls):
cls.running = False
if cls.session:
cls.session.close()
@classmethod
async def start(
cls,
on_audio: Optional[Callable[[bytes], Awaitable[None]]] = None,
on_text: Optional[Callable[[str], Awaitable[None]]] = None,
set_is_speaking: Optional[Callable[[bool], Awaitable[None]]] = None,
):
if cls.running:
return
cls.running = True
async with cls.client.aio.live.connect(
model=GEMINI_MODEL, config=cls.config
) as session:
cls.session = session
print("🔊 AI: ", "session connected")
while True:
print("🔊 AI: ", "waiting for response")
if not cls.running:
break
if set_is_speaking:
await set_is_speaking(False)
async for response in session.receive():
if response.server_content is None:
continue
if response.server_content.interrupted is True:
continue
if set_is_speaking:
await set_is_speaking(True)
if response.data is not None:
print("🔊 AI: ", len(response.data))
if on_audio:
await on_audio(response.data)
if response.text is not None:
print("✅ AI: ", response.text)
if on_text:
await on_text(response.text)
+51
View File
@@ -0,0 +1,51 @@
import asyncio
import numpy as np
import open_xiaoai_server
from gemini import Gemini
class XiaoAi:
loop: asyncio.AbstractEventLoop
is_ai_speaking = False
speaking_count = 0
@classmethod
async def start(cls):
cls.loop = asyncio.get_event_loop()
open_xiaoai_server.register_fn("on_input_data", cls.input_audio)
await open_xiaoai_server.start_server()
@classmethod
async def output_audio(cls, data: bytes):
await open_xiaoai_server.on_output_data(data)
@classmethod
def input_audio(cls, data: bytes):
if cls.is_ai_speaking:
# 如果 AI 正在回答问题,则不发送音频,防止把 AI 的声音被当做输入录制进来
# 暂不支持中断 AI 的回复,需要等待 AI 回答完成后才能重新响应用户的语音输入
return
async def send_audio_task():
audio_array = np.frombuffer(data, dtype=np.uint16)
await Gemini.send_audio(audio_array.tobytes())
asyncio.run_coroutine_threadsafe(send_audio_task(), cls.loop)
@classmethod
async def set_is_speaking(cls, is_speaking: bool):
if is_speaking:
cls.speaking_count += 1
cls.is_ai_speaking = True
return
# 延迟 1 秒,如果 AI 还在说话,则设置为不说话
async def set_is_speaking_task():
speaking_count = cls.speaking_count
await asyncio.sleep(1)
if cls.speaking_count == speaking_count:
cls.is_ai_speaking = False
asyncio.run_coroutine_threadsafe(set_is_speaking_task(), cls.loop)
+19
View File
@@ -0,0 +1,19 @@
import asyncio
from gemini.gemini import Gemini
from gemini.xiaoai import XiaoAi
async def main():
xiaoai_task = asyncio.create_task(XiaoAi.start())
gemini_task = asyncio.create_task(
Gemini.start(
on_audio=XiaoAi.output_audio,
set_is_speaking=XiaoAi.set_is_speaking,
)
)
await asyncio.gather(xiaoai_task, gemini_task)
if __name__ == "__main__":
asyncio.run(main())
+29
View File
@@ -0,0 +1,29 @@
[project]
name = "open-xiaoai-gemini"
version = "1.0.0"
description = "小爱音箱接入 Gemini Live API"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"asyncio>=3.4.3",
"google-genai>=1.12.1",
"httpx[socks]>=0.28.1",
"maturin>=1.8.3",
"numpy>=2.2.3",
]
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[tool.maturin]
features = ["pyo3/extension-module"]
[tool.uv]
# Rebuild package when any rust files change
# https://github.com/PyO3/maturin/issues/2314#issuecomment-2488542771
cache-keys = [
{ file = "pyproject.toml" },
{ file = "Cargo.toml" },
{ file = "src/**/*.rs" },
]
@@ -1,4 +1,5 @@
use open_xiaoai::base::{AppError, VERSION};
use open_xiaoai::services::audio::config::AudioConfig;
use open_xiaoai::services::connect::data::{Event, Request, Response, Stream};
use open_xiaoai::services::connect::handler::MessageHandler;
use open_xiaoai::services::connect::message::{MessageManager, WsStream};
@@ -19,10 +20,34 @@ async fn test() -> Result<(), AppError> {
SpeakerManager::play_text("已连接").await?;
let _ = RPC::instance()
.call_remote("start_recording", None, None)
.call_remote(
"start_recording",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 16000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
let _ = RPC::instance().call_remote("start_play", None, None).await;
let _ = RPC::instance()
.call_remote(
"start_play",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 24000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
Ok(())
}
+434
View File
@@ -0,0 +1,434 @@
version = 1
revision = 1
requires-python = ">=3.12"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
name = "asyncio"
version = "3.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767 },
]
[[package]]
name = "cachetools"
version = "5.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[package]]
name = "google-auth"
version = "2.39.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachetools" },
{ name = "pyasn1-modules" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/8e/8f45c9a32f73e786e954b8f9761c61422955d23c45d1e8c347f9b4b59e8e/google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7", size = 274834 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/12/ad37a1ef86006d0a0117fc06a4a00bd461c775356b534b425f00dde208ea/google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2", size = 212319 },
]
[[package]]
name = "google-genai"
version = "1.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "google-auth" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/9c/c907dbea921663bb7c41f415337bedd08259d17da8d156396c7237611744/google_genai-1.12.1.tar.gz", hash = "sha256:5c7eda422360643ce602a3f6b23152470ec1039310ef40080cbe4e71237f6391", size = 167752 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/2c/5b454dec837328eb167e78f45a14da502af223f8b94a4824e2fd0df74f19/google_genai-1.12.1-py3-none-any.whl", hash = "sha256:7cbc1bc029712946ce41bcf80c0eaa89eb8c09c308efbbfe30fd491f402c258a", size = 165940 },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[package.optional-dependencies]
socks = [
{ name = "socksio" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "maturin"
version = "1.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/0b/3fd746cf5cfa3c8d7e20ea08c0dbc2c2c765ae051d0fc43d808a38bc9548/maturin-1.8.3.tar.gz", hash = "sha256:304762f86fd53a8031b1bf006d12572a2aa0a5235485031113195cc0152e1e12", size = 199656 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/80/08579d0184ba743345bbd350a3b6419510ff8a4f6e9e671f713160d41fbb/maturin-1.8.3-py3-none-linux_armv6l.whl", hash = "sha256:fa27466b627150123729b2e611f9f9cfade84d24385d72c6877f78c30de30e89", size = 7758366 },
{ url = "https://files.pythonhosted.org/packages/87/1c/00755d28ae277daa828e183c3d118e2923e8b8f0cba4ff708b15d274ac0e/maturin-1.8.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:583404d20d7f1d9c8f3c18dcab9014faacabbed6be02da80062c06cd0e279554", size = 15201378 },
{ url = "https://files.pythonhosted.org/packages/58/9f/b8738dc55ba3eb149ad2686d3f9b3c24e44b7baff46cc6baa85e5dc8cf5e/maturin-1.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f9ffdac53dfe0089cf19b597410bc552eb34c856ddb41482b243a695e8b549d3", size = 7934215 },
{ url = "https://files.pythonhosted.org/packages/18/68/300f1a279486d6f63b624a76b81f0fba82545d027127c1ca4d5ded0d1b43/maturin-1.8.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7949a4a17637341f84e88f4cbf0c155998780bbb7a145ed735725b907881c0ae", size = 7791270 },
{ url = "https://files.pythonhosted.org/packages/2e/6d/bf1b8bb9a8b1d9adad242b4089794be318446142975762d04f04ffabae40/maturin-1.8.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:11564fac7486313b7baf3aa4e82c20e1b20364aad3fde2ccbc4c07693c0b7e16", size = 8282824 },
{ url = "https://files.pythonhosted.org/packages/18/e9/c601de8699e546774d3f9e761eab374988f88e4ed7006afbb5ff61bb41c0/maturin-1.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8555d8701cdba6c19c4705a22ce4d2a1814efd792f55dc5873262ff903317540", size = 7595855 },
{ url = "https://files.pythonhosted.org/packages/34/b8/82ae650e6b589289f4219a480f2b220bcf3d9391ae9ea02cc7a58ef59cfc/maturin-1.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:5b2a513468c1c9b4d1728d4b6d3d044b7c183985ef819c9ef15e373b70d99c7d", size = 7634888 },
{ url = "https://files.pythonhosted.org/packages/ac/4a/4f898a66cf4697267c447a6f240b6cbcc2dcacd3cab89735bfbd44810be1/maturin-1.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:2fe8fdff420cfccde127bbc3d8e40835163dcebb6d28c49fffd9aaf1e6ec1090", size = 9810616 },
{ url = "https://files.pythonhosted.org/packages/31/bd/21b0d471e6ade0801f27c33672bca4d563eb1d1e624e534f3bef8a01b1ac/maturin-1.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22cd8b6dc490fee99a62590f914f0c04ddad1dd6dbd5c7e933b3f882b1bd78c2", size = 10923701 },
{ url = "https://files.pythonhosted.org/packages/6c/07/85c32f03ed757c0626e2fc5a9d6454988228be96a756991702a2a757691b/maturin-1.8.3-py3-none-win32.whl", hash = "sha256:2427924546c9d1079b1c0c6c5c1d380e1b8187baba4e6342cca81597a0f3d697", size = 7016502 },
{ url = "https://files.pythonhosted.org/packages/d5/62/f92a130a370dd7aca13c316844b82853647f048cfe1594a81f628ab7101f/maturin-1.8.3-py3-none-win_amd64.whl", hash = "sha256:85f2b882d8235c1c1cb0a38d382ccd5b3ba0674d99cb548d49df9342cc688e36", size = 7953286 },
{ url = "https://files.pythonhosted.org/packages/04/95/8379140838cd95472de843e982d0bf674e8dbf25a899c44e2f76b15704d9/maturin-1.8.3-py3-none-win_arm64.whl", hash = "sha256:33939aabf9a06a8a14ca6c399d32616c7e574fcca8d4ff6dcd984441051f32fb", size = 6687772 },
]
[[package]]
name = "numpy"
version = "2.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/30/182db21d4f2a95904cec1a6f779479ea1ac07c0647f064dea454ec650c42/numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4", size = 20947156 },
{ url = "https://files.pythonhosted.org/packages/24/6d/9483566acfbda6c62c6bc74b6e981c777229d2af93c8eb2469b26ac1b7bc/numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854", size = 14133092 },
{ url = "https://files.pythonhosted.org/packages/27/f6/dba8a258acbf9d2bed2525cdcbb9493ef9bae5199d7a9cb92ee7e9b2aea6/numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24", size = 5163515 },
{ url = "https://files.pythonhosted.org/packages/62/30/82116199d1c249446723c68f2c9da40d7f062551036f50b8c4caa42ae252/numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee", size = 6696558 },
{ url = "https://files.pythonhosted.org/packages/0e/b2/54122b3c6df5df3e87582b2e9430f1bdb63af4023c739ba300164c9ae503/numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba", size = 14084742 },
{ url = "https://files.pythonhosted.org/packages/02/e2/e2cbb8d634151aab9528ef7b8bab52ee4ab10e076509285602c2a3a686e0/numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592", size = 16134051 },
{ url = "https://files.pythonhosted.org/packages/8e/21/efd47800e4affc993e8be50c1b768de038363dd88865920439ef7b422c60/numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb", size = 15578972 },
{ url = "https://files.pythonhosted.org/packages/04/1e/f8bb88f6157045dd5d9b27ccf433d016981032690969aa5c19e332b138c0/numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f", size = 17898106 },
{ url = "https://files.pythonhosted.org/packages/2b/93/df59a5a3897c1f036ae8ff845e45f4081bb06943039ae28a3c1c7c780f22/numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00", size = 6311190 },
{ url = "https://files.pythonhosted.org/packages/46/69/8c4f928741c2a8efa255fdc7e9097527c6dc4e4df147e3cadc5d9357ce85/numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146", size = 12644305 },
{ url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 },
{ url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 },
{ url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 },
{ url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 },
{ url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 },
{ url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 },
{ url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 },
{ url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 },
{ url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 },
{ url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 },
{ url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 },
{ url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 },
{ url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 },
{ url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 },
{ url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 },
{ url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 },
{ url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 },
{ url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 },
{ url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 },
{ url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 },
]
[[package]]
name = "open-xiaoai-gemini"
version = "1.0.0"
source = { editable = "." }
dependencies = [
{ name = "asyncio" },
{ name = "google-genai" },
{ name = "httpx", extra = ["socks"] },
{ name = "maturin" },
{ name = "numpy" },
]
[package.metadata]
requires-dist = [
{ name = "asyncio", specifier = ">=3.4.3" },
{ name = "google-genai", specifier = ">=1.12.1" },
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
{ name = "maturin", specifier = ">=1.8.3" },
{ name = "numpy", specifier = ">=2.2.3" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 },
]
[[package]]
name = "pydantic"
version = "2.11.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 },
]
[[package]]
name = "pydantic-core"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 },
{ url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 },
{ url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 },
{ url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 },
{ url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 },
{ url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 },
{ url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 },
{ url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 },
{ url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 },
{ url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 },
{ url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 },
{ url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 },
{ url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 },
{ url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 },
{ url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 },
{ url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 },
{ url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 },
{ url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 },
{ url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 },
{ url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 },
{ url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 },
{ url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 },
{ url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 },
{ url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 },
{ url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 },
{ url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 },
{ url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 },
{ url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 },
{ url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 },
{ url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 },
{ url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "rsa"
version = "4.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "socksio"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763 },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
]
[[package]]
name = "typing-inspection"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
]
[[package]]
name = "urllib3"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
]
+149
View File
@@ -0,0 +1,149 @@
# Open-XiaoAI x Sherpa-ONNX
小爱音箱自定义唤醒词,基于 [sherpa-onnx](https://github.com/k2-fsa/sherpa-onnx)。
## 快速开始
> [!NOTE]
> 以下操作需要先将小爱音箱刷机, 然后 SSH 连接到小爱音箱。👉 [教程](../../docs/flash.md)
```shell
# 创建 open-xiaoai/kws 文件夹
mkdir -p /data/open-xiaoai/kws
# 设置自定义唤醒词
cat <<EOF > /data/open-xiaoai/kws/keywords.txt
t iān m āo j īng l íng @天猫精灵
x iǎo d ù x iǎo d ù @小度小度
d òu b āo d òu b āo @豆包豆包
n ǐ h ǎo x iǎo zh ì @你好小智
EOF
# 设置唤醒提示语(可选)
cat <<EOF > /data/open-xiaoai/kws/reply.txt
主人你好,请问有什么吩咐?
EOF
# 运行启动脚本 init.sh
curl -sSfL https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws/init.sh | sh
```
如果你想要开机自启动,运行以下命令重启小爱音箱即可。
```shell
# 下载 boot.sh 到 /data/init.sh 开机时自启动
curl -L -o /data/init.sh https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws/boot.sh
# 重启小爱音箱
reboot
```
## 设置唤醒词
在你的电脑上克隆本项目,然后修改 `my-keywords.txt` 文件里的唤醒词。注意:
- 仅支持中文唤醒词
- 支持多个唤醒词,每行一个
运行以下命令,更新 `keywords.txt` 配置文件:
```shell
# 你可能需要先安装 uv 👉 https://github.com/astral-sh/uv
uv run keywords.py --tokens tokens.txt --output keywords.txt --text my-keywords.txt
```
然后将你电脑上的 `keywords.txt` 复制到小爱音箱 `/data/open-xiaoai/kws/keywords.txt`
如果你不方便复制文件,也可以直接在小爱音箱上运行以下命令(记得修改成自己的唤醒词)。
```shell
cat <<EOF > /data/open-xiaoai/kws/keywords.txt
t iān m āo j īng l íng @天猫精灵
x iǎo d ù x iǎo d ù @小度小度
d òu b āo d òu b āo @豆包豆包
n ǐ h ǎo x iǎo zh ì @你好小智
EOF
```
> [!TIP]
> 修改完毕后,记得重启脚本或小爱音箱使新配置生效。
## 设置欢迎语
你也可以设置自定义唤醒词唤醒后的提示语。首先新建一个 `reply.txt` 文件:
```txt
主人你好,请问有什么吩咐?
https://example.com/music.wav
file:///usr/share/sound-vendor/AiNiRobot/wakeup_ei_01.wav
```
注意:
- 每行一句欢迎语
- 支持文字和音频链接
- 多条欢迎语会随机选择一句播放
- 默认欢迎语与小爱同学一致:“哎”、“在”
然后将你电脑上的 `reply.txt`复制到小爱音箱 `/data/open-xiaoai/kws/reply.txt`
如果你不方便复制文件,也可以直接在小爱音箱上运行以下命令(记得修改成自己的欢迎语)。
```shell
cat <<EOF > /data/open-xiaoai/kws/reply.txt
主人你好,请问有什么吩咐?
https://example.com/music.wav
file:///usr/share/sound-vendor/AiNiRobot/wakeup_ei_01.wav
EOF
```
> [!TIP]
> 修改完毕后,记得重启脚本或小爱音箱使新配置生效。
## 常见问题
### Q:为什么唤醒词识别不是很灵敏,时好时坏?
由于小爱音箱 client 端算力和存储空间有限,默认只使用了一个较小的语音识别模型,所以识别效果并不是很完美,需要多多尝试。
你可以在小爱音箱上运行以下调试脚本,根据语音识别结果,动态调整唤醒词拼音。
```shell
# 运行调试脚本
curl -sSfL https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws/debug.sh | sh
# 🐢 模型加载较慢,请在提示 Started! Please speak 后,再使用自定义唤醒词
# Started! Please speak
# 0:tiānmāojīnglián 👈 天猫精灵
# 1:xiǎodùxiǎodù 👈 小度小度
# 2:dōubāodùba 👈 豆包豆包
# 3:nǐhǎoxiǎozhī 👈 你好小智
```
如果你想要更完美的唤醒词识别效果,可以在 server 端运行更大规模、更先进的 AI 模型,来进行唤醒词识别。推荐使用 [FunASR](https://github.com/modelscope/FunASR) 和 [Sherpa-ONNX](https://github.com/k2-fsa/sherpa-onnx),可以参考 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) 项目。
### Q:能不能设置英文/方言作为唤醒词?比如:Siri
默认使用的小型语音识别模型,只支持中文(普通话)作为唤醒词。
如果你想要使用英文唤醒词(比如:siri),或者更灵敏的唤醒词识别效果,可以参考这个 [Server 端演示](../xiaozhi/README.md)。
如果你需要更多语言的唤醒词,比如日语、韩语或方言等,可以在 server 端运行更大规模、更先进的 AI 模型,来进行唤醒词识别。推荐使用 [FunASR](https://github.com/modelscope/FunASR) 和 [Sherpa-ONNX](https://github.com/k2-fsa/sherpa-onnx),可以参考 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) 项目。
### Q:是否支持说话人(声纹)识别?
由于小爱音箱 client 端算力和存储空间有限,默认只使用了一个较小的语音识别模型,并不支持说话人识别等高阶功能。
如果你需要声纹识别、连续对话等高阶功能,可以在 server 端运行更大规模、更先进的 AI 模型,来进行语音识别和对话管理。推荐使用 [FunASR](https://github.com/modelscope/FunASR) 和 [Sherpa-ONNX](https://github.com/k2-fsa/sherpa-onnx),可以参考 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) 项目。
### Q:能否自定义唤醒词的响应动作?比如:关灯
该脚本默认不支持自定义唤醒词触发后执行的动作。
如果你想要自定义唤醒词的触发动作,可以参考 [open-xiaoai](https://github.com/idootop/open-xiaoai) 里的其他演示程序,根据 server 端接收到的小爱音箱上的唤醒词事件,实时触发任意自定义操作(比如:打电话,订外卖等)。
### Q:我不是小爱音箱 Pro 能不能用?
由于小爱音箱 client 端算力和存储空间有限,默认只使用了一个较小的[语音识别模型](https://k2-fsa.github.io/sherpa/onnx/kws/pretrained_models/index.html#sherpa-onnx-kws-pre-trained-models)(5MB 左右),需要占用大约 20 MB 的存储空间。其他型号的小爱音箱存储空间和算力可能不足以运行该模型,请自行测试。
你也可以参考 [open-xiaoai](https://github.com/idootop/open-xiaoai) 里的演示程序,在 server 端运行更大规模、更先进的 AI 模型,来进行唤醒词识别,达到相同目的。推荐使用 [FunASR](https://github.com/modelscope/FunASR) 和 [Sherpa-ONNX](https://github.com/k2-fsa/sherpa-onnx),可以参考 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) 项目。
+85
View File
@@ -0,0 +1,85 @@
#! /bin/sh
set -e
MIN_SPACE_MB=32
DOWNLOAD_BASE_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws"
check_disk_space() {
local space_kb=$(df -k "$1" | awk 'NR==2 {print $4}')
if [ $((space_kb / 1024)) -lt "$MIN_SPACE_MB" ]; then
echo 1
else
echo 0
fi
}
BASE_DIR="/data"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
BASE_DIR="/tmp"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
echo "❌ 磁盘空间不足,请先清理磁盘空间(至少需要 $MIN_SPACE_MB MB 空间)"
exit 1
fi
fi
WORK_DIR="$BASE_DIR/open-xiaoai/kws"
KWS_BIN="$WORK_DIR/kws"
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$WORK_DIR/models/encoder.onnx" ]; then
echo "🔥 正在下载模型文件..."
curl -L -# -o "$WORK_DIR/kws.tar.gz" "$DOWNLOAD_BASE_URL/kws.tar.gz"
tar -xzvf "$WORK_DIR/kws.tar.gz" -C "$WORK_DIR"
chmod +x "$KWS_BIN"
rm "$WORK_DIR/kws.tar.gz"
echo "✅ 模型文件下载完毕"
fi
CONFIG_DIR="/data/open-xiaoai/kws"
if [ ! -d "$CONFIG_DIR" ]; then
mkdir -p "$CONFIG_DIR"
fi
if [ ! -f "$CONFIG_DIR/keywords.txt" ]; then
echo "n ǐ h ǎo x iǎo zh ì @你好小智" >> "$CONFIG_DIR/keywords.txt"
echo "d òu b āo d òu b āo @豆包豆包" >> "$CONFIG_DIR/keywords.txt"
echo "t iān m āo j īng l íng @天猫精灵" >> "$CONFIG_DIR/keywords.txt"
echo "x iǎo d ù x iǎo d ù @小度小度" >> "$CONFIG_DIR/keywords.txt"
echo "✅ 默认唤醒词已创建"
fi
if [ "$1" != "--no-monitor" ]; then
# 如果没有设置 --no-monitor 参数,则启动 monitor 进程
MONITOR_BIN="$CONFIG_DIR/monitor"
if [ ! -f "$MONITOR_BIN" ]; then
curl -L -# -o "$MONITOR_BIN" "$DOWNLOAD_BASE_URL/monitor"
chmod +x "$MONITOR_BIN"
fi
kill -9 `ps|grep "open-xiaoai/kws/monitor"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
"$MONITOR_BIN" > /dev/null 2>&1 &
fi
echo "🔥 正在启动唤醒词识别服务,请耐心等待..."
echo "🐢 模型加载较慢,请在语音提示初始化成功后,再使用自定义唤醒词"
kill -9 `ps|grep "open-xiaoai/kws/kws"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
"$KWS_BIN" \
--model-type=zipformer2 \
--tokens="$WORK_DIR/models/tokens.txt" \
--encoder="$WORK_DIR/models/encoder.onnx" \
--decoder="$WORK_DIR/models/decoder.onnx" \
--joiner="$WORK_DIR/models/joiner.onnx" \
--keywords-file="/data/open-xiaoai/kws/keywords.txt" \
--provider=cpu \
--num-threads=1 \
--chunk-size=1024 \
noop
+64
View File
@@ -0,0 +1,64 @@
#! /bin/sh
set -e
MIN_SPACE_MB=32
DOWNLOAD_BASE_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws"
check_disk_space() {
local space_kb=$(df -k "$1" | awk 'NR==2 {print $4}')
if [ $((space_kb / 1024)) -lt "$MIN_SPACE_MB" ]; then
echo 1
else
echo 0
fi
}
BASE_DIR="/data"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
BASE_DIR="/tmp"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
echo "❌ 磁盘空间不足,请先清理磁盘空间(至少需要 $MIN_SPACE_MB MB 空间)"
exit 1
fi
fi
WORK_DIR="$BASE_DIR/open-xiaoai/kws"
KWS_DEBUG_BIN="$WORK_DIR/kws-debug"
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$WORK_DIR/models/encoder.onnx" ]; then
echo "🔥 正在下载模型文件..."
curl -L -# -o "$WORK_DIR/kws.tar.gz" "$DOWNLOAD_BASE_URL/kws.tar.gz"
tar -xzvf "$WORK_DIR/kws.tar.gz" -C "$WORK_DIR"
rm "$WORK_DIR/kws.tar.gz"
echo "✅ 模型文件下载完毕"
fi
if [ ! -f "$KWS_DEBUG_BIN" ]; then
echo "🔥 正在下载 kws-debug 文件..."
curl -L -# -o "$KWS_DEBUG_BIN" "$DOWNLOAD_BASE_URL/kws-debug"
chmod +x "$KWS_DEBUG_BIN"
echo "✅ kws-debug 文件下载完毕"
fi
echo "🔥 正在启动唤醒词识别调试服务,请耐心等待..."
echo "🐢 模型加载较慢,请在提示 Started! Please speak 后,再使用自定义唤醒词"
kill -9 `ps|grep "open-xiaoai/kws/kws-debug"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
"$KWS_DEBUG_BIN" \
--model-type=zipformer2 \
--tokens="$WORK_DIR/models/tokens.txt" \
--encoder="$WORK_DIR/models/encoder.onnx" \
--decoder="$WORK_DIR/models/decoder.onnx" \
--joiner="$WORK_DIR/models/joiner.onnx" \
--provider=cpu \
--num-threads=1 \
--chunk-size=1024 \
noop
+85
View File
@@ -0,0 +1,85 @@
#! /bin/sh
set -e
MIN_SPACE_MB=32
DOWNLOAD_BASE_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-kws"
check_disk_space() {
local space_kb=$(df -k "$1" | awk 'NR==2 {print $4}')
if [ $((space_kb / 1024)) -lt "$MIN_SPACE_MB" ]; then
echo 1
else
echo 0
fi
}
BASE_DIR="/data"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
BASE_DIR="/tmp"
if [ $(check_disk_space "$BASE_DIR") -eq 1 ]; then
echo "❌ 磁盘空间不足,请先清理磁盘空间(至少需要 $MIN_SPACE_MB MB 空间)"
exit 1
fi
fi
WORK_DIR="$BASE_DIR/open-xiaoai/kws"
KWS_BIN="$WORK_DIR/kws"
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$WORK_DIR/models/encoder.onnx" ]; then
echo "🔥 正在下载模型文件..."
curl -L -# -o "$WORK_DIR/kws.tar.gz" "$DOWNLOAD_BASE_URL/kws.tar.gz"
tar -xzvf "$WORK_DIR/kws.tar.gz" -C "$WORK_DIR"
chmod +x "$KWS_BIN"
rm "$WORK_DIR/kws.tar.gz"
echo "✅ 模型文件下载完毕"
fi
CONFIG_DIR="/data/open-xiaoai/kws"
if [ ! -d "$CONFIG_DIR" ]; then
mkdir -p "$CONFIG_DIR"
fi
if [ ! -f "$CONFIG_DIR/keywords.txt" ]; then
echo "n ǐ h ǎo x iǎo zh ì @你好小智" >> "$CONFIG_DIR/keywords.txt"
echo "d òu b āo d òu b āo @豆包豆包" >> "$CONFIG_DIR/keywords.txt"
echo "t iān m āo j īng l íng @天猫精灵" >> "$CONFIG_DIR/keywords.txt"
echo "x iǎo d ù x iǎo d ù @小度小度" >> "$CONFIG_DIR/keywords.txt"
echo "✅ 默认唤醒词已创建"
fi
if [ "$1" != "--no-monitor" ]; then
# 如果没有设置 --no-monitor 参数,则启动 monitor 进程
MONITOR_BIN="$CONFIG_DIR/monitor"
if [ ! -f "$MONITOR_BIN" ]; then
curl -L -# -o "$MONITOR_BIN" "$DOWNLOAD_BASE_URL/monitor"
chmod +x "$MONITOR_BIN"
fi
kill -9 `ps|grep "open-xiaoai/kws/monitor"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
"$MONITOR_BIN" &
fi
echo "🔥 正在启动唤醒词识别服务,请耐心等待..."
echo "🐢 模型加载较慢,请在语音提示初始化成功后,再使用自定义唤醒词"
kill -9 `ps|grep "open-xiaoai/kws/kws"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
"$KWS_BIN" \
--model-type=zipformer2 \
--tokens="$WORK_DIR/models/tokens.txt" \
--encoder="$WORK_DIR/models/encoder.onnx" \
--decoder="$WORK_DIR/models/decoder.onnx" \
--joiner="$WORK_DIR/models/joiner.onnx" \
--keywords-file="/data/open-xiaoai/kws/keywords.txt" \
--provider=cpu \
--num-threads=1 \
--chunk-size=1024 \
noop
+163
View File
@@ -0,0 +1,163 @@
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pypinyin",
# ]
# [tool.uv]
# exclude-newer = "2025-05-06T00:00:00Z"
# ///
import argparse
from pathlib import Path
from typing import List, Union
from pypinyin import pinyin
from pypinyin.contrib.tone_convert import to_initials, to_finals_tone
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--text",
type=str,
required=False,
default="my-keywords.txt",
help="""Path to the input texts.
Each line in the texts contains the original phrase, it might also contain some
extra items, for example, the boosting score (startting with :), the triggering
threshold (startting with #, only used in keyword spotting task) and the original
phrase (startting with @). Note: extra items will be kept in the output.
example input 1 (tokens_type = ppinyin):
小爱同学 :2.0 #0.6 @小爱同学
你好问问 :3.5 @你好问问
小艺小艺 #0.6 @小艺小艺
example output 1:
x iǎo ài t óng x ué :2.0 #0.6 @小爱同学
n ǐ h ǎo w èn w èn :3.5 @你好问问
x iǎo y ì x iǎo y ì #0.6 @小艺小艺
example input 2 (tokens_type = bpe):
HELLO WORLD :1.5 #0.4
HI GOOGLE :2.0 #0.8
HEY SIRI #0.35
example output 2:
▁HE LL O ▁WORLD :1.5 #0.4
▁HI ▁GO O G LE :2.0 #0.8
▁HE Y ▁S I RI #0.35
""",
)
parser.add_argument(
"--tokens",
type=str,
required=False,
default="tokens.txt",
help="The path to tokens.txt.",
)
parser.add_argument(
"--output",
type=str,
required=False,
default="keywords.txt",
help="Path where the encoded tokens will be written to.",
)
return parser.parse_args()
def text2token(
texts: List[str],
tokens: str,
output_ids: bool = False,
):
"""将文本转换为 token 列表"""
assert Path(tokens).is_file(), f"File not exists, {tokens}"
tokens_table = {}
with open(tokens, "r", encoding="utf-8") as f:
for line in f:
toks = line.strip().split()
assert len(toks) == 2, len(toks)
assert toks[0] not in tokens_table, f"Duplicate token: {toks} "
tokens_table[toks[0]] = int(toks[1])
texts_list: List[List[str]] = []
for txt in texts:
res = []
py = [x[0] for x in pinyin(txt)]
for x in py:
initial = to_initials(x, strict=False)
final = to_finals_tone(x, strict=False)
if initial == "" and final == "":
res.append(x)
else:
if initial != "":
res.append(initial)
if final != "":
res.append(final)
texts_list.append(res)
result: List[List[Union[int, str]]] = []
for text in texts_list:
text_list = []
contain_oov = False
for txt in text:
if txt in tokens_table:
text_list.append(tokens_table[txt] if output_ids else txt)
else:
print(
f"Can't find token {txt} in token table, check your "
f"tokens.txt see if {txt} in it. skipping text : {text}."
)
contain_oov = True
break
if contain_oov:
continue
else:
result.append(text_list)
return result
def main() -> None:
args = get_args()
texts = []
# extra information like boosting score (start with :), triggering threshold (start with #)
# original keyword (start with @)
extra_info = []
with open(args.text, "r", encoding="utf8") as f:
for line in f:
extra = []
text = []
toks = line.strip().split()
if len(toks) == 1:
text.append(toks[0])
extra.append('@'+toks[0])
else:
for tok in toks:
if tok[0] == ":" or tok[0] == "#" or tok[0] == "@":
extra.append(tok)
else:
text.append(tok)
texts.append(" ".join(text))
extra_info.append(extra)
encoded_texts = text2token(
texts,
tokens=args.tokens,
)
with open(args.output, "w", encoding="utf8") as f:
for i, txt in enumerate(encoded_texts):
txt += extra_info[i]
f.write(" ".join(txt) + "\n")
print(f"✅ 唤醒词已保存到 {args.output}")
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
t iān m āo j īng l íng @天猫精灵
x iǎo d ù x iǎo d ù @小度小度
d òu b āo d òu b āo @豆包豆包
n ǐ h ǎo x iǎo zh ì @你好小智
+4
View File
@@ -0,0 +1,4 @@
天猫精灵
小度小度
豆包豆包
你好小智
+227
View File
@@ -0,0 +1,227 @@
<blk> 0
<sos/eos> 1
<unk> 2
A 3
B 4
S 5
I 6
f 7
ù 8
ǔ 9
zh 10
y 11
ī 12
sh 13
ēng 14
uè 15
p 16
iàn 17
K 18
L 19
P 20
R 21
T 22
M 23
j 24
W 25
q 26
s 27
ān 28
b 29
ā 30
l 31
íng 32
iǔ 33
ch 34
ǐ 35
ì 36
èr 37
w 38
r 39
án 40
én 41
ó 42
g 43
uǎn 44
c 45
āng 46
ǎn 47
x 48
iān 49
ōng 50
àng 51
òng 52
d 53
ài 54
iù 55
ìng 56
ū 57
m 58
àn 59
ǎ 60
í 61
z 62
k 63
ǒu 64
ūn 65
uó 66
èng 67
òu 68
ú 69
éng 70
à 71
iào 72
āo 73
ào 74
h 75
uáng 76
t 77
iáo 78
è 79
iǎn 80
er 81
n 82
èi 83
ǐng 84
uā 85
ēi 86
ǎo 87
iú 88
uàng 89
uí 90
ǜ 91
āi 92
óu 93
ià 94
ǎng 95
īn 96
uà 97
uǐ 98
ái 99
ò 100
ē 101
óng 102
uàn 103
é 104
ìn 105
ùn 106
uò 107
ún 108
uì 109
uān 110
ǐn 111
iāo 112
ōu 113
uán 114
iǎo 115
á 116
ér 117
èn 118
C 119
D 120
E 121
O 122
N 123
Z 124
ěi 125
ián 126
ēn 127
iā 128
ǚ 129
áng 130
iě 131
ǒng 132
éi 133
iāng 134
J 135
V 136
ín 137
ié 138
īng 139
ō 140
ěr 141
F 142
ué 143
iǎng 144
uō 145
G 146
ǎi 147
ěn 148
H 149
en 150
X 151
i 152
ǒ 153
uǒ 154
iè 155
iū 156
U 157
iàng 158
uāng 159
uá 160
Q 161
ě 162
e 163
iǎ 164
iáng 165
Y 166
○ 167
iē 168
uī 169
ěng 170
uài 171
áo 172
a 173
ou 174
ǔn 175
uái 176
uāi 177
ióng 178
ei 179
ń 180
iá 181
iōng 182
üè 183
uē 184
uǎng 185
uǎ 186
o 187
uǎi 188
uě 189
ǘ 190
uo 191
iǒng 192
ang 193
ia 194
u 195
üě 196
#0 197
#1 198
#2 199
#3 200
#4 201
#5 202
#6 203
#7 204
#8 205
#9 206
#10 207
#11 208
#12 209
#13 210
#14 211
#15 212
#16 213
#17 214
#18 215
#19 216
#20 217
#21 218
#22 219
#23 220
#24 221
#25 222
#26 223
#27 224
#28 225
#29 226
@@ -390,6 +390,7 @@ name = "open-xiaoai"
version = "1.0.0"
dependencies = [
"futures",
"rand",
"serde",
"serde_json",
"tokio",
@@ -480,13 +481,12 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
"zerocopy",
]
[[package]]
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
[dependencies]
neon = { version = "1.1.0-alpha.1", features = ["napi-6", "futures", "tokio"] }
open-xiaoai = { path = "../client-rust" }
open-xiaoai = { path = "../../packages/client-rust" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
serde = { version = "1.0", features = ["derive"] }
+64
View File
@@ -0,0 +1,64 @@
FROM ubuntu:22.04 AS builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
WORKDIR /app
COPY examples/migpt .
COPY packages/client-rust ./client-rust
# 更新源
RUN apt update && apt install curl build-essential -y
# 安装 Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# Create a script file sourced by both interactive and non-interactive bash shells
ENV BASH_ENV=/root/.bash_env
RUN touch "$BASH_ENV"
RUN echo '. "$BASH_ENV"' >> "$HOME/.bashrc"
RUN echo '[ -s "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"' >> "$BASH_ENV"
# nvm
RUN echo 'export NVM_DIR="$HOME/.nvm"' >> "$BASH_ENV"
RUN echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"' >> "$BASH_ENV"
RUN echo '[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"' >> "$BASH_ENV"
# 安装 Node.js
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash \
&& \. $NVM_DIR/nvm.sh && \. $NVM_DIR/bash_completion \
&& nvm install 22
# 安装依赖
RUN npm install -g corepack \
&& corepack enable && corepack install \
&& pnpm install
# 构建 Rust Server
RUN sed -i 's/\.\.\/\.\.\/packages\///g' Cargo.toml \
&& pnpm build
FROM ubuntu:22.04 AS runner
WORKDIR /app
COPY --from=builder /app/migpt /app/migpt
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/tsconfig.json /app/tsconfig.json
COPY --from=builder /app/node_modules /app/node_modules
# 更新源
RUN apt update && apt install curl build-essential -y
ENV NVM_DIR=/root/.nvm
# 安装 Node.js
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash \
&& bash -c "source $NVM_DIR/nvm.sh && nvm install 22"
ENTRYPOINT ["bash", "-c", "source $NVM_DIR/nvm.sh && exec \"$@\"", "--"]
CMD ["npm", "run", "start"]
+83
View File
@@ -0,0 +1,83 @@
# Open-XiaoAI x MiGPT-Next
[Open-XiaoAI](https://github.com/idootop/open-xiaoai) 的 Node.js 版 Server 端,用来演示小爱音箱接入[MiGPT](https://github.com/idootop/mi-gpt)(完美版)。
相比原版的 `MiGPT``MiGPT-Next` 项目,该版本可以完美打断小爱音箱的回复,响应延迟更低,效果更完美 👍
## 快速开始
> [!NOTE]
> 继续下面的操作之前,你需要先在小爱音箱上启动运行 Rust 补丁程序 [👉 教程](../../packages/client-rust/README.md)
首先,克隆仓库代码到本地。
```shell
# 克隆代码
git clone https://github.com/idootop/open-xiaoai.git
# 进入当前项目根目录
cd examples/migpt
```
然后把 `config.ts` 文件里的配置修改成你自己的。
```typescript
export const kOpenXiaoAIConfig = {
openai: {
model: "gpt-4.1-mini",
baseURL: "https://api.openai.com/v1",
apiKey: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
},
prompt: {
system: "你是一个智能助手,请根据用户的问题给出回答。",
},
async onMessage(engine, { text }) {
if (text === "测试") {
return { text: "你好,很高兴认识你!" };
}
},
};
```
### Docker 运行
[![Docker Image Version](https://img.shields.io/docker/v/idootop/open-xiaoai-migpt?color=%23086DCD&label=docker%20image)](https://hub.docker.com/r/idootop/open-xiaoai-migpt)
推荐使用以下命令,直接 Docker 一键运行。
```shell
docker run -it --rm -p 4399:4399 -v $(pwd)/config.ts:/app/config.ts idootop/open-xiaoai-migpt:latest
```
### 编译运行
> [!TIP]
> 如果你是一名开发者,想要修改源代码实现自己想要的功能,可以按照下面的步骤,自行编译运行该项目。
为了能够正常编译运行该项目,你需要安装以下依赖环境:
- Node.js v22.x: https://nodejs.org/zh-cn/download
- Rust: https://www.rust-lang.org/learn/get-started
准备好开发环境后,按以下步骤即可正常启动该项目。
```bash
# 启用 PNPM 包管理工具
corepack enable && corepack install
# 安装依赖
pnpm install
# 编译运行
pnpm dev
```
## 注意事项
1. 默认 Server 服务端口为 `4399`(比如 ws://192.168.31.227:4399),运行前请确保该端口未被其他程序占用。
2. 默认 Rust Server 在启动时,并没有开启小爱音箱的录音能力。
如果你需要在 Node.js 端正常接收音频输入流,或者播放音频输出流,请将 `src/server.rs` 文件中被注释掉的 `start_recording``start_play` 代码加回来,然后重新编译运行。
> [!NOTE]
> 本项目只是一个简单的演示程序,抛砖引玉。如果你想要更多的功能,比如唤醒词识别、语音转文字、连续对话等(甚至是对接 OpenAI 的 [Realtime API](https://platform.openai.com/docs/guides/realtime)),可参考本项目代码自行实现。
+72
View File
@@ -0,0 +1,72 @@
import { sleep } from "@mi-gpt/utils";
import { OpenXiaoAIConfig } from "./migpt/xiaoai.js";
export const kOpenXiaoAIConfig: OpenXiaoAIConfig = {
openai: {
/**
* 你的大模型服务提供商的接口地址
*
* 支持兼容 OpenAI 接口的大模型服务,比如:DeepSeek V3 等
*
* 注意:一般以 /v1 结尾,不包含 /chat/completions 部分
* - ✅ https://api.openai.com/v1
* - ❌ https://api.openai.com/v1/(最后多了一个 /
* - ❌ https://api.openai.com/v1/chat/completions(不需要加 /chat/completions
*/
baseURL: "https://api.openai.com/v1",
/**
* API 密钥
*/
apiKey: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
/**
* 模型名称
*/
model: "gpt-4.1-mini",
},
prompt: {
/**
* 系统提示词,如需关闭可设置为:''(空字符串)
*/
system: "你是一个智能助手,请根据用户的问题给出回答。",
},
context: {
/**
* 每次对话携带的最大历史消息数(如需关闭可设置为:0)
*/
historyMaxLength: 10,
},
/**
* 只回答以下关键词开头的消息:
*
* - 请问地球为什么是圆的?
* - 你知道世界上跑的最快的动物是什么吗?
*/
callAIKeywords: ["请", "你"],
/**
* 自定义消息回复
*/
async onMessage(engine, { text }) {
if (text === "测试播放文字") {
return { text: "你好,很高兴认识你!" };
}
if (text === "测试播放音乐") {
return { url: "https://example.com/hello.mp3" };
}
if (text === "测试其他能力") {
// 打断原来小爱的回复
await engine.speaker.abortXiaoAI();
// 播放文字
await sleep(2000); // 打断小爱后需要等待 2 秒,使其恢复运行后才能继续 TTS
await engine.speaker.play({ text: "你好,很高兴认识你!", blocking: true });
// 播放音频链接
await engine.speaker.play({ url: "https://example.com/hello.mp3" });
// 告诉 MiGPT 已经处理过这条消息了,不再使用默认的 AI 回复
return { handled: true };
}
},
};
+9
View File
@@ -0,0 +1,9 @@
import { kOpenXiaoAIConfig } from "config.js";
import { OpenXiaoAI } from "./xiaoai.js";
async function main() {
await OpenXiaoAI.start(kOpenXiaoAIConfig);
process.exit(0);
}
main();
@@ -71,7 +71,7 @@ class SpeakerManager implements ISpeaker {
const res = await this.runShell(
url
? `miplayer -f '${url}'`
: `/usr/sbin/tts_play2.sh '${text || "你好"}'`,
: `/usr/sbin/tts_play.sh '${text || "你好"}'`,
{ timeout }
);
return res?.exit_code === 0;
@@ -104,7 +104,7 @@ class SpeakerManager implements ISpeaker {
silent: boolean;
}
) {
const { silent = false } = options ?? {};
const { silent = true } = options ?? {};
const command = awake
? silent
? `ubus call pnshelper event_notify '{"src":1,"event":0}'`
@@ -6,7 +6,7 @@ import { RustServer } from "./open-xiaoai.js";
import { OpenXiaoAISpeaker } from "./speaker.js";
import { randomUUID } from "node:crypto";
type OpenXiaoAIConfig = Prettify<EngineConfig<OpenXiaoAIEngine>>;
export type OpenXiaoAIConfig = Prettify<EngineConfig<OpenXiaoAIEngine>>;
const kDefaultOpenXiaoAIConfig: OpenXiaoAIConfig = {
//
@@ -57,6 +57,9 @@ class OpenXiaoAIEngine extends MiGPTEngine {
timestamp: Date.now(),
});
}
} else if (e.event === "kws") {
const keyword = e.data;
console.log("🔥 唤醒词识别", keyword);
}
};
@@ -1,6 +1,7 @@
{
"name": "open-xiaoai-node",
"name": "open-xiaoai-migpt",
"version": "1.0.0",
"description": "小爱音箱接入 MiGPT-Next",
"private": true,
"type": "module",
"scripts": {
@@ -9,11 +10,11 @@
"dev": "pnpm build && pnpm start"
},
"dependencies": {
"@mi-gpt/engine": "1.1.1",
"@mi-gpt/utils": "1.0.0"
"@mi-gpt/engine": "1.2.1",
"@mi-gpt/utils": "1.1.1"
},
"devDependencies": {
"@mi-gpt/config": "1.0.0",
"@mi-gpt/config": "1.1.0",
"@neon-rs/cli": "0.1.82",
"@types/node": "^22.13.11",
"cargo-cp-artifact": "^0.1.9",
@@ -9,15 +9,15 @@ importers:
.:
dependencies:
'@mi-gpt/engine':
specifier: 1.2.1
version: 1.2.1
'@mi-gpt/utils':
specifier: 1.1.1
version: 1.1.1
'@mi-gpt/utils':
specifier: 1.0.0
version: 1.0.0
devDependencies:
'@mi-gpt/config':
specifier: 1.0.0
version: 1.0.0
specifier: 1.1.0
version: 1.1.0
'@neon-rs/cli':
specifier: 0.1.82
version: 0.1.82
@@ -236,28 +236,28 @@ packages:
cpu: [x64]
os: [win32]
'@mi-gpt/chat@1.0.0':
resolution: {integrity: sha512-PyyQ4Vg3s1OcUxr9GXZTpBgGCRQldhm9vGKc3UpAA4QDyVJWSP6ILhXKfFHO+UfnxDr45y9wlk5PpMcgkmPNkQ==}
'@mi-gpt/chat@1.1.1':
resolution: {integrity: sha512-C5b498nGC1ZAg6/Nl5XWneOwnUQ9CgK+0wCEqNDHYkj96YWqpbW3uGS6ubgNncpVMg/uXMoc55l1hsUF0lz+oQ==}
engines: {node: '>=16'}
'@mi-gpt/config@1.0.0':
resolution: {integrity: sha512-oo/roYiRcGiHsZESjukwPTsRFqddShTJ8gkccR5ZMYn8SySj07A0nCsEsLEfB9mLkvNY8D6PCO1NxbRDDRBBlA==}
'@mi-gpt/config@1.1.0':
resolution: {integrity: sha512-o24iFINsoDuvInnMpfBQ4zCuTObbdodjXyizXmxl3yyYOMjmIAXizO/1UZ9/dv3UVTov3sjL09aCGuBeIVhvtA==}
engines: {node: '>=16'}
'@mi-gpt/engine@1.1.1':
resolution: {integrity: sha512-TyA6JCq/yGvJI+ysFuDOwLaDUJ1fjjtxHIGB3Nyr40R0Ujb32Ssz/FoCDpbVmTKj2niebZQAGuUl7/juBuQwtw==}
'@mi-gpt/engine@1.2.1':
resolution: {integrity: sha512-EmBVS/l1xj8N54ZHRmYcV0jl9vh0QBPsyMdjwmS5h9+cdG40ItUq+shVg9ZHJrwoixKIiagYJ4lAjw+yUQ1DUQ==}
engines: {node: '>=16'}
'@mi-gpt/openai@1.0.0':
resolution: {integrity: sha512-BZI2gJkcJ4TuHcyyD5fzvFzEMlSkfkkuxpycQ/mSiMC8Z3umQbRSk2z1Tg3ho8DFxf5tYiIsFzbvWCBw2WXlwg==}
'@mi-gpt/openai@1.1.1':
resolution: {integrity: sha512-pEs9y/GMY/ZDmlFfcBuowuvyE+j3cTKeQY0cMhZ9AobXD/qGNWlP7gQXojsSbVg/D0bB2snhEiWv/X5s1EzJWg==}
engines: {node: '>=16'}
'@mi-gpt/stream@1.0.0':
resolution: {integrity: sha512-nzYKGODEhTbBUv6WGmgEY4rioXfWRvs2te99nQWjl+agmN9bT7KTbHKm8tzTDApQcQltx5BTDXrXMairmLVTYQ==}
'@mi-gpt/stream@1.1.1':
resolution: {integrity: sha512-4zd8brCqGX05tStweUWFdeE4EVpZGtFXHfvfYqvdJWccFK4PZkW3ynvJ0lc4N8SdsJo5Ml6XxOftRpZUnfGJtg==}
engines: {node: '>=16'}
'@mi-gpt/utils@1.0.0':
resolution: {integrity: sha512-hAU/uYPNiNJgXWkQrVqRLe/9Uz8AgtoiNwcbbwQs8iXJcyq3NjcSHkkwUseJAnaQFE9iv/kvHZZsx3r2HhlKjw==}
'@mi-gpt/utils@1.1.1':
resolution: {integrity: sha512-FiGHV2RDBRGE/Xb9Q5H757t3ZqOpo6gL+44VXiRxQb3fQiapOpn038RAWErHJYZajYzkxP2jl8Wggsb+wp77Zg==}
engines: {node: '>=16'}
'@neon-rs/cli@0.1.82':
@@ -671,34 +671,34 @@ snapshots:
'@esbuild/win32-x64@0.25.2':
optional: true
'@mi-gpt/chat@1.0.0':
'@mi-gpt/chat@1.1.1':
dependencies:
'@mi-gpt/openai': 1.0.0
'@mi-gpt/stream': 1.0.0
'@mi-gpt/utils': 1.0.0
'@mi-gpt/openai': 1.1.1
'@mi-gpt/stream': 1.1.1
'@mi-gpt/utils': 1.1.1
transitivePeerDependencies:
- encoding
- supports-color
- ws
- zod
'@mi-gpt/config@1.0.0': {}
'@mi-gpt/config@1.1.0': {}
'@mi-gpt/engine@1.1.1':
'@mi-gpt/engine@1.2.1':
dependencies:
'@mi-gpt/chat': 1.0.0
'@mi-gpt/openai': 1.0.0
'@mi-gpt/stream': 1.0.0
'@mi-gpt/utils': 1.0.0
'@mi-gpt/chat': 1.1.1
'@mi-gpt/openai': 1.1.1
'@mi-gpt/stream': 1.1.1
'@mi-gpt/utils': 1.1.1
transitivePeerDependencies:
- encoding
- supports-color
- ws
- zod
'@mi-gpt/openai@1.0.0':
'@mi-gpt/openai@1.1.1':
dependencies:
'@mi-gpt/utils': 1.0.0
'@mi-gpt/utils': 1.1.1
openai: 4.93.0
proxy-agent: 6.5.0
transitivePeerDependencies:
@@ -707,11 +707,11 @@ snapshots:
- ws
- zod
'@mi-gpt/stream@1.0.0':
'@mi-gpt/stream@1.1.1':
dependencies:
'@mi-gpt/utils': 1.0.0
'@mi-gpt/utils': 1.1.1
'@mi-gpt/utils@1.0.0': {}
'@mi-gpt/utils@1.1.1': {}
'@neon-rs/cli@0.1.82':
optionalDependencies:
@@ -6,6 +6,6 @@
"@/*": ["migpt/*"]
}
},
"include": ["migpt"],
"include": ["migpt", "config.ts"],
"exclude": ["node_modules"]
}
+1
View File
@@ -0,0 +1 @@
3.12
+25
View File
@@ -0,0 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: App",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"justMyCode": false,
"console": "internalConsole",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Python: Current",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"justMyCode": false,
"console": "internalConsole",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env"
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"python.languageServer": "Pylance",
"python.analysis.autoImportCompletions": true,
"ruff.organizeImports": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
}
},
"files.exclude": {
"**/.git": true,
"**/node_modules": true,
"**/__pycache__": true,
"*.egg-info": true,
".mypy_cache": true
}
}
+903
View File
@@ -0,0 +1,903 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "open-xiaoai"
version = "1.0.0"
dependencies = [
"futures",
"rand",
"serde",
"serde_json",
"tokio",
"tokio-tungstenite",
"uuid",
]
[[package]]
name = "open_xiaoai_server"
version = "1.0.0"
dependencies = [
"open-xiaoai",
"pyo3",
"pyo3-async-runtimes",
"serde",
"serde_json",
"tokio",
"tokio-tungstenite",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-async-runtimes"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0b83dc42f9d41f50d38180dad65f0c99763b65a3ff2a81bf351dd35a1df8bf"
dependencies = [
"futures",
"once_cell",
"pin-project-lite",
"pyo3",
"pyo3-async-runtimes-macros",
"tokio",
]
[[package]]
name = "pyo3-async-runtimes-macros"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf103ba4062fbb1e8022d9ed9b9830fbab074b2db0a0496c78e45a62f4330bcd"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pyo3-build-config"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
dependencies = [
"bitflags",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "open_xiaoai_server"
version = "1.0.0"
edition = "2021"
[lib]
name = "open_xiaoai_server"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.24.0", features = ["extension-module", "abi3-py38"] }
pyo3-async-runtimes = { version = "0.24", features = [
"attributes",
"tokio-runtime",
] }
open-xiaoai = { path = "../../packages/client-rust" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+57
View File
@@ -0,0 +1,57 @@
FROM python:3.12-slim AS builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# 更新源
RUN apt-get update && apt-get install -y --no-install-recommends \
curl build-essential libopus-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装 Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# 安装 uv
COPY --from=ghcr.io/astral-sh/uv:0.7 /uv /uvx /bin/
# 设置环境变量
ENV BASH_ENV=/root/.bash_env
RUN touch "$BASH_ENV"
RUN echo '. "$BASH_ENV"' >> "$HOME/.bashrc"
RUN echo '[ -s "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"' >> "$BASH_ENV"
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY examples/xiaozhi .
COPY packages/client-rust ./client-rust
# 构建
RUN sed -i 's/\.\.\/\.\.\/packages\///g' Cargo.toml \
&& cargo build --release
# 安装依赖
RUN --mount=type=cache,target=/root/.cache/uv \
uv remove pyaudio && uv sync --locked --no-install-project --no-editable
# 构建 wheel 并安装
RUN uv run maturin build --release && uv remove maturin
FROM python:3.12-slim
WORKDIR /app
# 更新源
RUN apt-get update && apt-get install -y --no-install-recommends \
libopus-dev \
&& rm -rf /var/lib/apt/lists/*
ENV CLI=true
COPY --from=builder /app/.venv /app/.venv
COPY examples/xiaozhi/main.py .
COPY examples/xiaozhi/xiaozhi ./xiaozhi
# 先初始化关键词模型,然后启动主程序
CMD ["/bin/bash", "-c", "source /app/.venv/bin/activate && python xiaozhi/services/audio/kws/keywords.py && python main.py"]
+162
View File
@@ -0,0 +1,162 @@
# Open-XiaoAI x 小智 AI
[Open-XiaoAI](https://github.com/idootop/open-xiaoai) 的 Python 版 Server 端,用来演示小爱音箱接入[小智 AI](https://github.com/78/xiaozhi-esp32)。
- 小爱音箱接入小智 AI
- 自定义唤醒词(中英文)和提示语
- 支持连续对话和中途打断
- 支持自定义消息处理,方便个人定制
## 快速开始
> [!NOTE]
> 继续下面的操作之前,你需要先在小爱音箱上启动运行 Rust 补丁程序 [👉 教程](../../packages/client-rust/README.md)
首先,克隆仓库代码到本地。
```shell
# 克隆代码
git clone https://github.com/idootop/open-xiaoai.git
# 进入当前项目根目录
cd examples/xiaozhi
```
然后把 `config.py` 文件里的配置修改成你自己的。
```typescript
APP_CONFIG = {
"wakeup": {
#
"keywords": [
"豆包豆包",
"你好小智",
"hi siri",
],
},
"xiaozhi": {
"OTA_URL": "https://api.tenclass.net/xiaozhi/ota/",
"WEBSOCKET_URL": "wss://api.tenclass.net/xiaozhi/v1/",
},
}
```
### Docker 运行
[![Docker Image Version](https://img.shields.io/docker/v/idootop/open-xiaoai-xiaozhi?color=%23086DCD&label=docker%20image)](https://hub.docker.com/r/idootop/open-xiaoai-xiaozhi)
推荐使用以下命令,直接 Docker 一键运行。
```shell
docker run -it --rm -p 4399:4399 -v $(pwd)/config.py:/app/config.py idootop/open-xiaoai-xiaozhi:latest
```
### 编译运行
为了能够正常编译运行该项目,你需要安装以下依赖环境/工具:
- uvhttps://github.com/astral-sh/uv
- Rust: https://www.rust-lang.org/learn/get-started
- [Opus](https://opus-codec.org/): 自行询问 AI 如何安装动态链接库,或参考[这篇文章](https://github.com/huangjunsen0406/py-xiaozhi/blob/3bfd2887244c510a13912c1d63263ae564a941e9/documents/docs/guide/01_%E7%B3%BB%E7%BB%9F%E4%BE%9D%E8%B5%96%E5%AE%89%E8%A3%85.md#2-opus-%E9%9F%B3%E9%A2%91%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8)
```bash
# 安装 Python 依赖
uv sync --locked
# 编译运行
uv run main.py
```
如果你只是想体验一下小智 AI,请使用以下命令启动:
```bash
uv run main.py --mode xiaozhi
```
该模式下使用电脑的麦克风和扬声器作为音频输入输出设备,无需连接小爱音箱。
> [!NOTE]
> 本项目只是一个简单的演示程序,抛砖引玉。诸如一些音频压缩、加密传输、多账号管理等功能并未提供,请自行评估用途和使用风险。
## 常见问题
### Q:回答太长了,如何打断小智 AI 的回答?
直接召唤“小爱同学”,即可打断小智 AI 的回答 ;)
### Q:第一次运行提示我输入验证码绑定设备,如何操作?
第一次启动对话时,会有语音提示使用验证码绑定设备。请打开你的小智 AI [管理后台](https://xiaozhi.me/),然后根据提示创建 Agent 绑定设备即可。验证码消息会在终端打印,或者打开你的 `config.py` 文件查看。
```py
APP_CONFIG = {
"xiaozhi": {
"VERIFICATION_INFO": "首次登录时,验证码会在这里更新",
},
# ... 其他配置
}
```
PS:绑定设备成功后,可能需要重启应用才会生效。
### Q:怎样使用自己部署的 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server) 服务?
如果你想使用自己部署的 [xiaozhi-esp32-server](https://github.com/xinnan-tech/xiaozhi-esp32-server),请更新 `config.py` 文件里的接口地址,然后重启应用。
```py
APP_CONFIG = {
"xiaozhi": {
"OTA_URL": "https://2662r3426b.vicp.fun/xiaozhi/ota/",
"WEBSOCKET_URL": "wss://2662r3426b.vicp.fun/xiaozhi/v1/",
},
# ... 其他配置
}
```
### Q:有时候话还没说完 AI 就开始回答了,如何优化?
你可以调大 `config.py` 配置文件里的 `min_silence_duration` 参数,然后重启应用 / Docker 试试看。
```py
APP_CONFIG = {
"vad": {
# 最小静默时长(ms
"min_silence_duration": 1000,
},
# ... 其他配置
}
```
### Q:对话的时候,文字识别不是很准?
文字识别结果取决于你的小智 AI 服务器端的语音识别方案,与本项目无关。
不过需要注意的是,在唤醒后或连续对话时,由于 VAD 激活阈值的关系,开始的一小段语音可能会被丢弃识别不到,使用时需要多注意。
比如:“你知道我是谁吗” 有可能会被识别为“知道我是谁吗”(丢掉了最前面一个字)
### Q:唤醒词一直没有反应?
由于小爱音箱远场拾音音量较小,有时可能会识别不清,你可以调大 `config.py` 配置文件里的 `boost` 参数,然后重启应用 / Docker 试试看。
```py
APP_CONFIG = {
"vad": {
# 小爱音箱录音音量较小,需要后期放大一下
"boost": 100,
# boost 调大后,语音检测阈值可能也需要一起调大些
"threshold": 0.50,
},
# ... 其他配置
}
```
另外,应用 / Docker 刚刚启动时需要加载模型文件,比较耗时一些,可以等 30s 之后再试试看。
如果是英文唤醒词,可以尝试将最小发音用空格分开,比如:比如:'openai' 👉 'open ai'
PS:如果还是不行,建议更换其他更易识别的唤醒词。
## 鸣谢
该演示使用的 Python 版小智 AI 客户端基于 [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) 项目,特此鸣谢。
+79
View File
@@ -0,0 +1,79 @@
import asyncio
async def before_wakeup(speaker, text, source):
"""
处理收到的用户消息,并决定是否唤醒小智 AI
- source: 唤醒来源
- 'kws': 关键字唤醒
- 'xiaoai': 小爱同学收到用户指令
"""
if source == "kws" and text == "你好小智":
# 播放唤醒提示语
await speaker.play(text="你好主人,我是小智,请问有什么吩咐?")
# 返回 True 唤醒小智 AI
return True
if source == "kws" and text == "hi siri":
# 播放唤醒提示语
await speaker.play(text="I'm Siri, how can I help you?")
# 唤醒小爱
await speaker.wake_up()
return False
if source == "xiaoai" and text == "召唤小智":
# 打断原来的小爱同学
await speaker.abort_xiaoai()
# 等待 2 秒,让小爱 TTS 恢复可用
await asyncio.sleep(2)
# 播放唤醒提示语(如果你不使用自带的小爱 TTS,可以去掉上面的延时)
await speaker.play(text="小智来了,主人有什么吩咐?")
# 唤醒小智 AI
return True
async def after_wakeup(speaker):
"""
退出唤醒状态
"""
await speaker.play(text="主人再见,拜拜")
APP_CONFIG = {
"wakeup": {
# 自定义唤醒词列表(英文字母要全小写)
"keywords": [
"天猫精灵",
"小度小度",
"豆包豆包",
"你好小智",
"你好小爱",
"hi siri",
"hey siri",
],
# 静音多久后自动退出唤醒(秒)
"timeout": 20,
# 语音识别结果回调
"before_wakeup": before_wakeup,
# 退出唤醒时的提示语(设置为空可关闭)
"after_wakeup": after_wakeup,
},
"vad": {
# 录音音量增强倍数(小爱音箱录音音量较小,需要后期放大一下)
"boost": 10,
# 语音检测阈值(0-1,越小越灵敏)
"threshold": 0.10,
# 最小语音时长(ms
"min_speech_duration": 250,
# 最小静默时长(ms
"min_silence_duration": 500,
},
"xiaozhi": {
"OTA_URL": "https://api.tenclass.net/xiaozhi/ota/",
"WEBSOCKET_URL": "wss://api.tenclass.net/xiaozhi/v1/",
"WEBSOCKET_ACCESS_TOKEN": "(可选)一般用不到这个值",
"DEVICE_ID": "(可选)默认自动生成",
"VERIFICATION_CODE": "首次登陆时,验证码会在这里更新",
},
}
@@ -1,18 +1,11 @@
import logging
import signal
import sys
from xiaozhi.services.audio.setup import setup_opus
from xiaozhi.utils.logging_config import setup_logging
from xiaozhi.xiaoai import XiaoAi
from xiaozhi.xiaoai import XiaoAI
from xiaozhi.xiaozhi import XiaoZhi
logger = logging.getLogger("Main")
def main():
setup_opus()
logger.info("应用程序已启动,按Ctrl+C退出")
XiaoZhi.instance().run()
return 0
@@ -26,7 +19,6 @@ def setup_graceful_shutdown():
if __name__ == "__main__":
XiaoAi.setup_mode()
setup_logging()
XiaoAI.setup_mode()
setup_graceful_shutdown()
sys.exit(main())
@@ -1,17 +1,19 @@
[project]
name = "open-xiaoai-python"
name = "open-xiaoai-xiaozhi"
version = "1.0.0"
description = "小爱音箱接入小智 AI"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"asyncio>=3.4.3",
"cryptography>=44.0.2",
"maturin>=1.8.3",
"numpy>=2.2.3",
"opuslib>=3.0.1",
"onnxruntime>=1.22.0",
"opuslib-next>=1.1.4",
"pyaudio>=0.2.14",
"pypinyin>=0.54.0",
"requests>=2.32.3",
"sentencepiece>=0.2.0",
"sherpa-onnx>=1.12.0",
"websockets>=15.0.1",
]
+56
View File
@@ -0,0 +1,56 @@
use open_xiaoai::services::connect::message::MessageManager;
use open_xiaoai::services::connect::rpc::RPC;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use serde_json::json;
use server::AppServer;
pub mod macros;
pub mod python;
pub mod server;
#[pyfunction]
fn on_output_data(py: Python, data: Py<PyBytes>) -> PyResult<Bound<PyAny>> {
let bytes = data.as_bytes(py).to_vec();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let _ = MessageManager::instance()
.send_stream("play", bytes, None)
.await;
Ok(())
})
}
#[pyfunction]
fn start_server(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async {
AppServer::run().await;
Ok(())
})
}
#[pyfunction]
fn run_shell(py: Python, script: String, timeout_millis: f64) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let res = RPC::instance()
.call_remote(
"run_shell",
Some(json!(script)),
Some(timeout_millis as u64),
)
.await;
let result = match res {
Err(e) => format!("run_shell error: {}", e),
Ok(res) => serde_json::to_string(&res.data.unwrap()).unwrap(),
};
Ok(result)
})
}
#[pymodule]
fn open_xiaoai_server(_py: Python, m: Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(start_server, &m)?)?;
m.add_function(wrap_pyfunction!(on_output_data, &m)?)?;
m.add_function(wrap_pyfunction!(run_shell, &m)?)?;
crate::python::init_module(&m)?;
Ok(())
}
+14
View File
@@ -0,0 +1,14 @@
/// 提供一个简便的日志记录宏,将消息传递给 Python 端
///
/// # 示例
///
/// ```
/// pylog!("这是一条日志消息");
/// pylog!("带有变量的日志: {}", variable);
/// ```
#[macro_export]
macro_rules! pylog {
($($arg:tt)*) => {
crate::python::PythonManager::instance().log(format!($($arg)*));
};
}
+77
View File
@@ -0,0 +1,77 @@
use pyo3::prelude::*;
use std::collections::HashMap;
use std::ffi::CString;
use std::sync::{Arc, LazyLock, RwLock};
pub struct PythonManager {
functions: Arc<RwLock<HashMap<String, PyObject>>>,
}
static INSTANCE: LazyLock<PythonManager> = LazyLock::new(PythonManager::new);
impl PythonManager {
pub fn new() -> Self {
Self {
functions: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn instance() -> &'static Self {
&INSTANCE
}
fn register_fn(&self, key: &str, function: PyObject) -> PyResult<()> {
let mut functions = self.functions.write().unwrap();
functions.insert(key.to_string(), function);
Ok(())
}
fn unregister_fn(&self, key: &str) -> PyResult<()> {
let mut functions = self.functions.write().unwrap();
functions.remove(key);
Ok(())
}
pub fn call_fn(&self, key: &str, arg: Option<PyObject>) -> PyResult<PyObject> {
let functions = self.functions.read().unwrap();
if let Some(function) = functions.get(key) {
Python::with_gil(|py| match arg {
None => function.call0(py),
Some(arg) => function.call1(py, (arg,)),
})
} else {
Err(pyo3::exceptions::PyKeyError::new_err(format!(
"未找到函数: {}",
key
)))
}
}
pub fn log(&self, text: String) {
let _ = self.eval(&format!("print('{}')", text));
}
pub fn eval(&self, script: &str) -> PyResult<()> {
Python::with_gil(|py| {
let c_script = CString::new(script)?;
py.run(&c_script, None, None)?;
Ok(())
})
}
}
#[pyfunction]
fn register_fn(key: &str, function: PyObject) -> PyResult<()> {
PythonManager::instance().register_fn(key, function)
}
#[pyfunction]
fn unregister_fn(key: &str) -> PyResult<()> {
PythonManager::instance().unregister_fn(key)
}
pub fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(register_fn, m)?)?;
m.add_function(wrap_pyfunction!(unregister_fn, m)?)?;
Ok(())
}
+134
View File
@@ -0,0 +1,134 @@
use crate::python::PythonManager;
use open_xiaoai::base::{AppError, VERSION};
use open_xiaoai::services::audio::config::AudioConfig;
use open_xiaoai::services::connect::data::{Event, Request, Response, Stream};
use open_xiaoai::services::connect::handler::MessageHandler;
use open_xiaoai::services::connect::message::{MessageManager, WsStream};
use open_xiaoai::services::connect::rpc::RPC;
use open_xiaoai::services::speaker::SpeakerManager;
use open_xiaoai::utils::task::TaskManager;
use pyo3::types::PyBytes;
use pyo3::types::PyString;
use pyo3::Python;
use serde_json::json;
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::accept_async;
pub struct AppServer;
async fn test() -> Result<(), AppError> {
SpeakerManager::play_text("已连接").await?;
let _ = RPC::instance()
.call_remote(
"start_recording",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 16000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
let _ = RPC::instance()
.call_remote(
"start_play",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 24000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
Ok(())
}
impl AppServer {
pub async fn connect(stream: TcpStream) -> Result<WsStream, AppError> {
let ws_stream = accept_async(stream).await?;
Ok(WsStream::Server(ws_stream))
}
pub async fn run() {
let addr = "0.0.0.0:4399";
let listener = TcpListener::bind(&addr)
.await
.expect(format!("❌ 绑定地址失败: {}", &addr).as_str());
crate::pylog!("✅ 已启动: {:?}", addr);
while let Ok((stream, addr)) = listener.accept().await {
// 同一时刻只处理一个连接
AppServer::handle_connection(stream, addr).await;
}
}
async fn handle_connection(stream: TcpStream, addr: std::net::SocketAddr) {
let Ok(ws_stream) = AppServer::connect(stream).await else {
crate::pylog!("❌ 连接异常: {}", addr);
return;
};
crate::pylog!("✅ 已连接: {:?}", addr);
AppServer::init(ws_stream).await;
if let Err(e) = MessageManager::instance().process_messages().await {
crate::pylog!("❌ 消息处理异常: {}", e);
}
AppServer::dispose().await;
crate::pylog!("❌ 已断开连接");
}
async fn init(ws_stream: WsStream) {
MessageManager::instance().init(ws_stream).await;
MessageHandler::<Event>::instance()
.set_handler(on_event)
.await;
MessageHandler::<Stream>::instance()
.set_handler(on_stream)
.await;
let rpc = RPC::instance();
rpc.add_command("get_version", get_version).await;
let test = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let _ = test().await;
});
TaskManager::instance().add("test", test).await;
}
async fn dispose() {
MessageManager::instance().dispose().await;
TaskManager::instance().dispose("test").await;
}
}
async fn get_version(_: Request) -> Result<Response, AppError> {
let data = json!(VERSION.to_string());
Ok(Response::from_data(data))
}
async fn on_stream(stream: Stream) -> Result<(), AppError> {
let Stream { tag, bytes, .. } = stream;
match tag.as_str() {
"record" => {
let data = Python::with_gil(|py| PyBytes::new(py, &bytes).into());
PythonManager::instance().call_fn("on_input_data", Some(data))?;
}
_ => {}
}
Ok(())
}
async fn on_event(event: Event) -> Result<(), AppError> {
let event_json = serde_json::to_string(&event)?;
let data = Python::with_gil(|py| PyString::new(py, &event_json).into());
PythonManager::instance().call_fn("on_event", Some(data))?;
Ok(())
}
+163 -73
View File
@@ -20,39 +20,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
@@ -89,38 +56,36 @@ wheels = [
]
[[package]]
name = "cryptography"
version = "44.0.2"
name = "coloredlogs"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "humanfriendly" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
{ url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
{ url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
{ url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
{ url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
{ url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
{ url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
{ url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
{ url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
{ url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
{ url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
{ url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
{ url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
{ url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
{ url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
{ url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
{ url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
{ url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
{ url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
{ url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
{ url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
{ url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
{ url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
{ url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 },
]
[[package]]
name = "flatbuffers"
version = "25.2.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953 },
]
[[package]]
name = "humanfriendly"
version = "10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 },
]
[[package]]
@@ -152,6 +117,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/95/8379140838cd95472de843e982d0bf674e8dbf25a899c44e2f76b15704d9/maturin-1.8.3-py3-none-win_arm64.whl", hash = "sha256:33939aabf9a06a8a14ca6c399d32616c7e574fcca8d4ff6dcd984441051f32fb", size = 6687772 },
]
[[package]]
name = "mpmath"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
]
[[package]]
name = "numpy"
version = "2.2.4"
@@ -191,37 +165,94 @@ wheels = [
]
[[package]]
name = "open-xiaoai-python"
name = "onnxruntime"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coloredlogs" },
{ name = "flatbuffers" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/de/9162872c6e502e9ac8c99a98a8738b2fab408123d11de55022ac4f92562a/onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97", size = 34298046 },
{ url = "https://files.pythonhosted.org/packages/03/79/36f910cd9fc96b444b0e728bba14607016079786adf032dae61f7c63b4aa/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c", size = 14443220 },
{ url = "https://files.pythonhosted.org/packages/8c/60/16d219b8868cc8e8e51a68519873bdb9f5f24af080b62e917a13fff9989b/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c", size = 16406377 },
{ url = "https://files.pythonhosted.org/packages/36/b4/3f1c71ce1d3d21078a6a74c5483bfa2b07e41a8d2b8fb1e9993e6a26d8d3/onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a", size = 12692233 },
{ url = "https://files.pythonhosted.org/packages/a9/65/5cb5018d5b0b7cba820d2c4a1d1b02d40df538d49138ba36a509457e4df6/onnxruntime-1.22.0-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:fe7c051236aae16d8e2e9ffbfc1e115a0cc2450e873a9c4cb75c0cc96c1dae07", size = 34298715 },
{ url = "https://files.pythonhosted.org/packages/e1/89/1dfe1b368831d1256b90b95cb8d11da8ab769febd5c8833ec85ec1f79d21/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a6bbed10bc5e770c04d422893d3045b81acbbadc9fb759a2cd1ca00993da919", size = 14443266 },
{ url = "https://files.pythonhosted.org/packages/1e/70/342514ade3a33ad9dd505dcee96ff1f0e7be6d0e6e9c911fe0f1505abf42/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe45ee3e756300fccfd8d61b91129a121d3d80e9d38e01f03ff1295badc32b8", size = 16406707 },
{ url = "https://files.pythonhosted.org/packages/3e/89/2f64e250945fa87140fb917ba377d6d0e9122e029c8512f389a9b7f953f4/onnxruntime-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:5a31d84ef82b4b05d794a4ce8ba37b0d9deb768fd580e36e17b39e0b4840253b", size = 12691777 },
{ url = "https://files.pythonhosted.org/packages/9f/48/d61d5f1ed098161edd88c56cbac49207d7b7b149e613d2cd7e33176c63b3/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2ac5bd9205d831541db4e508e586e764a74f14efdd3f89af7fd20e1bf4a1ed", size = 14454003 },
{ url = "https://files.pythonhosted.org/packages/c3/16/873b955beda7bada5b0d798d3a601b2ff210e44ad5169f6d405b93892103/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64845709f9e8a2809e8e009bc4c8f73b788cee9c6619b7d9930344eae4c9cd36", size = 16427482 },
]
[[package]]
name = "open-xiaoai-xiaozhi"
version = "1.0.0"
source = { editable = "." }
dependencies = [
{ name = "asyncio" },
{ name = "cryptography" },
{ name = "maturin" },
{ name = "numpy" },
{ name = "opuslib" },
{ name = "onnxruntime" },
{ name = "opuslib-next" },
{ name = "pyaudio" },
{ name = "pypinyin" },
{ name = "requests" },
{ name = "sentencepiece" },
{ name = "sherpa-onnx" },
{ name = "websockets" },
]
[package.metadata]
requires-dist = [
{ name = "asyncio", specifier = ">=3.4.3" },
{ name = "cryptography", specifier = ">=44.0.2" },
{ name = "maturin", specifier = ">=1.8.3" },
{ name = "numpy", specifier = ">=2.2.3" },
{ name = "opuslib", specifier = ">=3.0.1" },
{ name = "onnxruntime", specifier = ">=1.22.0" },
{ name = "opuslib-next", specifier = ">=1.1.4" },
{ name = "pyaudio", specifier = ">=0.2.14" },
{ name = "pypinyin", specifier = ">=0.54.0" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "sentencepiece", specifier = ">=0.2.0" },
{ name = "sherpa-onnx", specifier = ">=1.12.0" },
{ name = "websockets", specifier = ">=15.0.1" },
]
[[package]]
name = "opuslib"
version = "3.0.1"
name = "opuslib-next"
version = "1.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/46/55/826befabb29fd3902bad6d6d7308790894c7ad4d73f051728a0c53d37cd7/opuslib-3.0.1.tar.gz", hash = "sha256:2cb045e5b03e7fc50dfefe431e3404dddddbd8f5961c10c51e32dfb69a044c97", size = 8550 }
sdist = { url = "https://files.pythonhosted.org/packages/16/c9/0440b1c4f98c1bf1575ca3e3e3e18872c80d44c184b31d8d9ed1d04b9323/opuslib_next-1.1.4.tar.gz", hash = "sha256:415322be6c738ffef9df6cd7e79725880c1737ccf82c538af5c17b7bb6da60e9", size = 9363 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/a8/3674c6f3af230d692752cb2b2534c84c9e2f2e48b1221aa3354289e8cbea/opuslib_next-1.1.4-py3-none-any.whl", hash = "sha256:aefaf2ca1d2e0f279a4236045096d21f374581f2c33bf35657570e75028f8a55", size = 13435 },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
name = "protobuf"
version = "6.30.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/8c/cf2ac658216eebe49eaedf1e06bc06cbf6a143469236294a1171a51357c3/protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048", size = 429315 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/85/cd53abe6a6cbf2e0029243d6ae5fb4335da2996f6c177bb2ce685068e43d/protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103", size = 419148 },
{ url = "https://files.pythonhosted.org/packages/97/e9/7b9f1b259d509aef2b833c29a1f3c39185e2bf21c9c1be1cd11c22cb2149/protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9", size = 431003 },
{ url = "https://files.pythonhosted.org/packages/8e/66/7f3b121f59097c93267e7f497f10e52ced7161b38295137a12a266b6c149/protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b", size = 417579 },
{ url = "https://files.pythonhosted.org/packages/d0/89/bbb1bff09600e662ad5b384420ad92de61cab2ed0f12ace1fd081fd4c295/protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815", size = 317319 },
{ url = "https://files.pythonhosted.org/packages/28/50/1925de813499546bc8ab3ae857e3ec84efe7d2f19b34529d0c7c3d02d11d/protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d", size = 316212 },
{ url = "https://files.pythonhosted.org/packages/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062 },
]
[[package]]
name = "pyaudio"
@@ -236,12 +267,21 @@ wheels = [
]
[[package]]
name = "pycparser"
version = "2.22"
name = "pypinyin"
version = "0.54.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
sdist = { url = "https://files.pythonhosted.org/packages/c9/7f/81cb5416ddacfeccca8eeedcd3543a72b093b26d9c4ca7bde8beea733e4e/pypinyin-0.54.0.tar.gz", hash = "sha256:9ab0d07ff51d191529e22134a60e109d0526d80b7a80afa73da4c89521610958", size = 837455 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
{ url = "https://files.pythonhosted.org/packages/b6/ec/2c04ac863e7a85bb68b0b655cec2f19853d51d305ce3d785848db6037b8d/pypinyin-0.54.0-py2.py3-none-any.whl", hash = "sha256:5f776f19b9fd922e4121a114810b22048d90e6e8037fb1c07f4c40f987ae6e7a", size = 837012 },
]
[[package]]
name = "pyreadline3"
version = "3.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 },
]
[[package]]
@@ -259,6 +299,56 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "sentencepiece"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/d2/b9c7ca067c26d8ff085d252c89b5f69609ca93fb85a00ede95f4857865d4/sentencepiece-0.2.0.tar.gz", hash = "sha256:a52c19171daaf2e697dc6cbe67684e0fa341b1248966f6aebb541de654d15843", size = 2632106 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/5a/141b227ed54293360a9ffbb7bf8252b4e5efc0400cdeac5809340e5d2b21/sentencepiece-0.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea5f536e32ea8ec96086ee00d7a4a131ce583a1b18d130711707c10e69601cb2", size = 2409370 },
{ url = "https://files.pythonhosted.org/packages/2e/08/a4c135ad6fc2ce26798d14ab72790d66e813efc9589fd30a5316a88ca8d5/sentencepiece-0.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0cb51f53b6aae3c36bafe41e86167c71af8370a039f542c43b0cce5ef24a68c", size = 1239288 },
{ url = "https://files.pythonhosted.org/packages/49/0a/2fe387f825ac5aad5a0bfe221904882106cac58e1b693ba7818785a882b6/sentencepiece-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3212121805afc58d8b00ab4e7dd1f8f76c203ddb9dc94aa4079618a31cf5da0f", size = 1181597 },
{ url = "https://files.pythonhosted.org/packages/cc/38/e4698ee2293fe4835dc033c49796a39b3eebd8752098f6bd0aa53a14af1f/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3149e3066c2a75e0d68a43eb632d7ae728c7925b517f4c05c40f6f7280ce08", size = 1259220 },
{ url = "https://files.pythonhosted.org/packages/12/24/fd7ef967c9dad2f6e6e5386d0cadaf65cda8b7be6e3861a9ab3121035139/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:632f3594d3e7ac8b367bca204cb3fd05a01d5b21455acd097ea4c0e30e2f63d7", size = 1355962 },
{ url = "https://files.pythonhosted.org/packages/4f/d2/18246f43ca730bb81918f87b7e886531eda32d835811ad9f4657c54eee35/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f295105c6bdbb05bd5e1b0cafbd78ff95036f5d3641e7949455a3f4e5e7c3109", size = 1301706 },
{ url = "https://files.pythonhosted.org/packages/8a/47/ca237b562f420044ab56ddb4c278672f7e8c866e183730a20e413b38a989/sentencepiece-0.2.0-cp312-cp312-win32.whl", hash = "sha256:fb89f811e5efd18bab141afc3fea3de141c3f69f3fe9e898f710ae7fe3aab251", size = 936941 },
{ url = "https://files.pythonhosted.org/packages/c6/97/d159c32642306ee2b70732077632895438867b3b6df282354bd550cf2a67/sentencepiece-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a673a72aab81fef5ebe755c6e0cc60087d1f3a4700835d40537183c1703a45f", size = 991994 },
]
[[package]]
name = "sherpa-onnx"
version = "1.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/81/64f21ee729ef0c21f645048c05201b0657b52ea2ada9e65ed2a1b69c1dd2/sherpa-onnx-1.12.0.tar.gz", hash = "sha256:91d912ad102bd4149ddd2dcb3a2bebfeecc34ad903eaeae89dc7fbf11103f62a", size = 530627 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/50/3ff72177343d538341091102f9e139f4a0bbb89797ae78ecd4c479d015f5/sherpa_onnx-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7efc658647bcbd053879415755eff5fd96964d97ac3385be89c197a98182aa", size = 18166756 },
{ url = "https://files.pythonhosted.org/packages/bd/13/a7cfdd1edba5339c68a5de8c20e0d8621fef962ac4171a79772cefc79a8f/sherpa_onnx-1.12.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:cf62d2ad4a494debb2335298ac38f7ce98a4821ce3397a1d35248cb3978a51d9", size = 38783152 },
{ url = "https://files.pythonhosted.org/packages/25/ec/ebb543554538633d29171de2264fdf5fd8eb680c04c98e7fa9e249cb57a5/sherpa_onnx-1.12.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:5bc6bac071bf7f8f83113c760f932d154fd636f8a5b56fe97e199f7b95a18272", size = 20664664 },
{ url = "https://files.pythonhosted.org/packages/ae/1c/55d16fad62cfbe24239eb963cd3c9ba9906ad6c56772eaeab84211c7f1ab/sherpa_onnx-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4084a131bab3b6b5974b7c6472b09048c7f0b2d0f9b0b26171de8bff5bf91aa8", size = 23013320 },
{ url = "https://files.pythonhosted.org/packages/34/5f/2b1b736e745911b745d323e970b0452238af537a79b4d10f40e1326d642c/sherpa_onnx-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d2078be1ef5d1d65822de21adca3b7e6353f3859dad02eb1cfd71c676e410ef", size = 24909403 },
{ url = "https://files.pythonhosted.org/packages/4c/88/06ef64d952c69c714c5f4fb22d83e86727f0567790663fd059446253ea72/sherpa_onnx-1.12.0-cp312-cp312-win32.whl", hash = "sha256:91d0aeaa1f6e5fb91b9b4a1326e67691a3f306a61a499a75f4c437697f8596ef", size = 20842198 },
{ url = "https://files.pythonhosted.org/packages/34/c1/e2ccebbf1931235850f734f420c0e719247648f53dc1011b528bb9bcef55/sherpa_onnx-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8df63f2235adc43fe735d07d0ca5e910337ebfeea03bc22931527b6a0231e505", size = 23667903 },
{ url = "https://files.pythonhosted.org/packages/ae/eb/4f8eeff8661933e8f6c69f44a5f29b49a1d7a156517737f0cf1c7dbfa54f/sherpa_onnx-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15725f20a19ef9fb31c73e79a2aad53f2b962bef0f13ed15fd6b02c71f8c4904", size = 18166754 },
{ url = "https://files.pythonhosted.org/packages/29/00/57f967161782b78ebbbdd29ea54060e17cece2818a9e3f93cdee18739555/sherpa_onnx-1.12.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74bc0bbc8695dff73acb620c0d3d4c5dd5d89aa0401d5cf31f6e15150b33947b", size = 38783224 },
{ url = "https://files.pythonhosted.org/packages/5c/0d/9186b91274e2d6711884bb88a6b3c8d5264d967e44c461839e90d9ceb931/sherpa_onnx-1.12.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:ced07e99b511b899d999d54f6f749c8200ef92df05cc6439dca8ca7da4f3800f", size = 20664831 },
{ url = "https://files.pythonhosted.org/packages/06/65/bb281e9309f83a7e69af403a06dc0ed6a96d9708adb486051d2dbdf782db/sherpa_onnx-1.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:111e270e7da787a578904268108de90284060a83c88721eac0de773407e84d97", size = 23013541 },
{ url = "https://files.pythonhosted.org/packages/ec/d4/2048c2e91881bdbb3139f60ccf52c511d8065d4a46ca102cbcd6081c79b7/sherpa_onnx-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a7ae7ce0d4b70417c03fdbcad8946eba739f2968688d87c501eea5bf97a909", size = 24909452 },
{ url = "https://files.pythonhosted.org/packages/42/d0/1485dd93cba8d018e82edbc21d63946e679f075335404367ed2ca9a4d5b0/sherpa_onnx-1.12.0-cp313-cp313-win32.whl", hash = "sha256:801b592a596c09a97e415813d5656a71a6f68655b000748f136cf66db69cbe4d", size = 20838807 },
{ url = "https://files.pythonhosted.org/packages/c4/5f/4a0cf4e8adc9d1aa299fbb87a379bc73737eb202bebccef9f0d09805e0d5/sherpa_onnx-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:46cf8ccd5d360e1b02a2668d8416b746c624b78f8b7346f61d4e9fb6eab53c07", size = 23667855 },
]
[[package]]
name = "sympy"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mpmath" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 },
]
[[package]]
name = "urllib3"
version = "2.3.0"
+159
View File
@@ -0,0 +1,159 @@
import asyncio
from config import APP_CONFIG
from xiaozhi.ref import get_audio_codec, get_speaker, get_vad, get_xiaoai, get_xiaozhi
from xiaozhi.services.protocols.typing import AbortReason, DeviceState, ListeningMode
class Step:
idle = "idle"
on_interrupt = "on_interrupt"
on_wakeup = "on_wakeup"
on_tts_start = "on_tts_start"
on_tts_end = "on_tts_end"
on_speech = "on_speech"
on_silence = "on_silence"
class __EventManager:
def __init__(self):
self.session_id = 0
self.current_step = Step.idle
self.next_step_future = None
def update_step(self, step: Step, step_data=None):
if get_xiaoai().mode == "xiaozhi":
return
self.current_step = step
if self.next_step_future:
get_xiaoai().async_loop.call_soon_threadsafe(
self.next_step_future.set_result, (step, step_data)
)
self.next_step_future = None
async def wait_next_step(self, timeout=None) -> Step | None:
current_session = self.session_id
self.next_step_future = get_xiaoai().async_loop.create_future()
async def _timeout(timeout):
idx = 0
while idx < timeout:
idx += 1
await asyncio.sleep(1)
return ("timeout", None)
futures = [self.next_step_future]
if timeout:
futures.append(get_xiaoai().async_loop.create_task(_timeout(timeout)))
done, _ = await asyncio.wait(
futures,
return_when=asyncio.FIRST_COMPLETED,
)
if current_session != self.session_id:
# 当前 session 已经结束
return ("interrupted", None)
return list(done)[0].result()
def on_interrupt(self):
"""用户打断(小爱同学)"""
self.session_id = self.session_id + 1
self.update_step(Step.on_interrupt)
self.start_session()
def on_wakeup(self):
"""用户唤醒(你好小智)"""
self.session_id = self.session_id + 1
self.update_step(Step.on_wakeup)
self.start_session()
def on_tts_end(self):
"""TTS结束"""
if self.current_step == Step.on_interrupt:
# 当前 session 已经被打断了,不再处理
return
self.session_id = self.session_id + 1
self.update_step(Step.on_tts_end)
self.start_session()
def on_tts_start(self):
"""TTS结束"""
self.update_step(Step.on_tts_start)
def on_speech(self, speech_buffer: bytes):
"""检测到声音(开始说话"""
self.update_step(Step.on_speech, speech_buffer)
def on_silence(self):
"""检测到静音(说话结束)"""
self.update_step(Step.on_silence)
def start_session(self):
asyncio.run_coroutine_threadsafe(
self.__start_session(), get_xiaoai().async_loop
)
async def __start_session(self):
if get_xiaoai().mode == "xiaozhi":
return
vad = get_vad()
codec = get_audio_codec()
speaker = get_speaker()
xiaozhi = get_xiaozhi()
# 先取消之前的 VAD 检测和音频输入输出流
xiaozhi.set_device_state(DeviceState.IDLE)
await xiaozhi.protocol.send_abort_speaking(AbortReason.ABORT)
# 小爱同学唤醒时,直接打断
if self.current_step == Step.on_interrupt:
return
# 尝试打开音频通道
if not xiaozhi.protocol.is_audio_channel_opened():
xiaozhi.set_device_state(DeviceState.CONNECTING)
await xiaozhi.protocol.open_audio_channel()
# 等待 TTS 余音结束
if self.current_step in [Step.on_tts_end]:
vad.resume("silence")
step, _ = await self.wait_next_step()
if step != Step.on_silence:
return
# 检查是否有人说话
vad.resume("speech")
step, speech_buffer = await self.wait_next_step(
timeout=APP_CONFIG["wakeup"]["timeout"]
)
if step == "timeout":
# 如果没人说话,则回到 IDLE 状态
xiaozhi.set_device_state(DeviceState.IDLE)
print("👋 已退出唤醒")
after_wakeup = APP_CONFIG["wakeup"]["after_wakeup"]
await after_wakeup(speaker)
return
if step != Step.on_speech:
return
# 开始说话
await xiaozhi.protocol.send_start_listening(ListeningMode.MANUAL)
codec.input_stream.input(speech_buffer) # 追加音频输入片段
xiaozhi.set_device_state(DeviceState.LISTENING)
# 等待说话结束
vad.resume("silence")
step, _ = await self.wait_next_step()
if step != Step.on_silence:
return
# 停止说话
await xiaozhi.protocol.send_stop_listening()
xiaozhi.set_device_state(DeviceState.IDLE)
EventManager = __EventManager()
@@ -0,0 +1,7 @@
天 猫 精 灵 @天猫精灵
小 度 小 度 @小度小度
豆 包 豆 包 @豆包豆包
你 好 小 智 @你好小智
你 好 小 爱 @你好小爱
▁HI ▁S I RI
▁HE Y ▁S I RI
+51
View File
@@ -0,0 +1,51 @@
from typing import Any
GLOBAL_STATES = {}
def set_xiaozhi(xiaozhi: Any):
GLOBAL_STATES["xiaozhi"] = xiaozhi
def get_xiaozhi() -> Any:
return GLOBAL_STATES.get("xiaozhi")
def set_xiaoai(xiaoai: Any):
GLOBAL_STATES["xiaoai"] = xiaoai
def get_xiaoai() -> Any:
return GLOBAL_STATES.get("xiaoai")
def set_vad(vad: Any):
GLOBAL_STATES["vad"] = vad
def get_vad() -> Any:
return GLOBAL_STATES.get("vad")
def set_audio_codec(opus_encoder: Any):
GLOBAL_STATES["opus_encoder"] = opus_encoder
def get_audio_codec() -> Any:
return GLOBAL_STATES.get("opus_encoder")
def get_speaker() -> Any:
return GLOBAL_STATES.get("speaker")
def set_speaker(speaker: Any):
GLOBAL_STATES["speaker"] = speaker
def set_kws(kws: Any):
GLOBAL_STATES["kws"] = kws
def get_kws() -> Any:
return GLOBAL_STATES.get("kws")
@@ -0,0 +1,153 @@
import opuslib_next as opuslib
from xiaozhi.ref import set_audio_codec
from xiaozhi.services.audio.stream import MyAudio
from xiaozhi.services.protocols.typing import AudioConfig
class AudioCodec:
"""音频编解码器类,处理音频的录制和播放"""
def __init__(self):
"""初始化音频编解码器"""
self.audio = None
self.input_stream = None
self.output_stream = None
self.opus_encoder = None
self.opus_decoder = None
self._is_closing = False
self._initialize_audio()
set_audio_codec(self)
def _initialize_audio(self):
"""初始化音频设备和编解码器"""
self.audio = MyAudio.create()
# 初始化音频输入流
self.input_stream = self.audio.open(
format=AudioConfig.FORMAT,
channels=AudioConfig.CHANNELS,
rate=AudioConfig.SAMPLE_RATE,
input=True,
frames_per_buffer=AudioConfig.FRAME_SIZE,
input_device_index=MyAudio.get_input_device_index(self.audio),
)
# 初始化音频输出流
self.output_stream = self.audio.open(
format=AudioConfig.FORMAT,
channels=AudioConfig.CHANNELS,
rate=AudioConfig.SAMPLE_RATE,
output=True,
frames_per_buffer=AudioConfig.FRAME_SIZE,
output_device_index=MyAudio.get_output_device_index(self.audio),
)
# 初始化Opus编码器
self.opus_encoder = opuslib.Encoder(
fs=AudioConfig.SAMPLE_RATE,
channels=AudioConfig.CHANNELS,
application=opuslib.APPLICATION_AUDIO,
)
# 初始化Opus解码器
self.opus_decoder = opuslib.Decoder(
fs=AudioConfig.SAMPLE_RATE, channels=AudioConfig.CHANNELS
)
def read_audio(self):
"""读取音频输入数据并编码"""
try:
data = self.input_stream.read(
AudioConfig.FRAME_SIZE, exception_on_overflow=False
)
if not data:
return None
return self.opus_encoder.encode(data, AudioConfig.FRAME_SIZE)
except Exception:
return None
def write_audio(self, opus_data):
"""解码并播放"""
try:
pcm_data = self.decode_audio(opus_data) # 解码
self.output_stream.write(pcm_data) # 播放
except Exception:
pass
def decode_audio(self, opus_data, frame_size=AudioConfig.FRAME_SIZE):
"""解码音频数据"""
return self.opus_decoder.decode(opus_data, frame_size, decode_fec=False)
def encode_audio(self, buffer, frame_size=AudioConfig.FRAME_SIZE):
"""编码音频数据"""
try:
opus_frames = []
for i in range(0, len(buffer), frame_size * 2):
chunk = buffer[i : i + frame_size * 2]
if len(chunk) < frame_size * 2:
# 如果 buffer 长度不是 FRAME_SIZE 的 2 倍,需要补齐
chunk += b"\x00" * (frame_size * 2 - len(chunk))
opus_frame = self.opus_encoder.encode(chunk, frame_size)
opus_frames.append(opus_frame)
return opus_frames
except Exception:
return None
def start_streams(self):
"""启动音频流"""
if not self.input_stream.is_active():
self.input_stream.start_stream()
if not self.output_stream.is_active():
self.output_stream.start_stream()
def stop_streams(self):
"""停止音频流"""
if self.input_stream.is_active():
self.input_stream.stop_stream()
if self.output_stream.is_active():
self.output_stream.stop_stream()
def close(self):
"""关闭音频编解码器,确保资源正确释放"""
if self._is_closing: # 防止重复关闭
return
self._is_closing = True
try:
# 关闭输入流
if self.input_stream:
try:
if self.input_stream.is_active():
self.input_stream.stop_stream()
self.input_stream.close()
except Exception:
pass
self.input_stream = None
# 关闭输出流
if self.output_stream:
try:
if self.output_stream.is_active():
self.output_stream.stop_stream()
self.output_stream.close()
except Exception:
pass
self.output_stream = None
# 关闭 PyAudio 实例
if self.audio:
try:
self.audio.terminate()
except Exception:
pass
self.audio = None
# 清理编解码器
self.opus_encoder = None
self.opus_decoder = None
except Exception:
pass
finally:
self._is_closing = False
@@ -0,0 +1,73 @@
import asyncio
import os
import threading
import time
from config import APP_CONFIG
from xiaozhi.event import EventManager
from xiaozhi.ref import get_speaker, get_xiaoai, get_xiaozhi, set_kws
from xiaozhi.services.audio.kws.sherpa import SherpaOnnx
from xiaozhi.services.audio.stream import MyAudio
from xiaozhi.services.protocols.typing import AudioConfig, DeviceState
from xiaozhi.utils.base import get_env
class _KWS:
def __init__(self):
set_kws(self)
def start(self):
if not get_env("CLI"):
return
self.audio = MyAudio.create()
self.stream = self.audio.open(
format=AudioConfig.FORMAT,
channels=1,
rate=16000,
input=True,
frames_per_buffer=AudioConfig.FRAME_SIZE,
start=True,
)
# 启动 KWS 服务
self.thread = threading.Thread(target=self._detection_loop, daemon=True)
self.thread.start()
def get_file_path(self, file_name: str):
current_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.join(current_dir, "../../../models", file_name)
def _detection_loop(self):
SherpaOnnx.start()
self.stream.start_stream()
while True:
# 读取缓冲区音频数据
frames = self.stream.read()
# 在说话和监听状态时,暂停 KWS
if not frames or get_xiaozhi().device_state in [
DeviceState.LISTENING,
DeviceState.SPEAKING,
]:
time.sleep(0.01)
continue
result = SherpaOnnx.kws(frames)
if result:
print(f"🔥 触发唤醒: {result}")
self.on_message(result)
def on_message(self, text: str):
asyncio.run_coroutine_threadsafe(
self._on_message(text), get_xiaoai().async_loop
)
async def _on_message(self, text: str):
before_wakeup = APP_CONFIG["wakeup"]["before_wakeup"]
wakeup = await before_wakeup(get_speaker(), text, "kws")
if wakeup:
EventManager.on_wakeup()
KWS = _KWS()
@@ -0,0 +1,52 @@
import re
from sherpa_onnx import text2token
def init_project_context():
"""动态导入父模块"""
import os
import sys
project_root = os.path.abspath(
os.path.join(os.path.dirname(__file__), "../../../..")
)
if project_root not in sys.path:
sys.path.insert(0, project_root)
init_project_context()
from config import APP_CONFIG
from xiaozhi.utils.file import get_model_file_path
def get_args():
tokens_type = "cjkchar+bpe"
tokens = get_model_file_path("tokens.txt")
bpe_model = get_model_file_path("bpe.model")
output = get_model_file_path("keywords.txt")
keywords = APP_CONFIG["wakeup"]["keywords"]
texts = [f"{keyword.upper()}" for keyword in keywords]
return locals()
def main():
args = get_args()
encoded_texts = text2token(
args["texts"],
tokens=args["tokens"],
tokens_type=args["tokens_type"],
bpe_model=args["bpe_model"],
)
with open(args["output"], "w", encoding="utf8") as f:
for _, txt in enumerate(encoded_texts):
line = "".join(txt)
if re.match(r"^[▁A-Z\s]+$", line):
f.write(" ".join(txt) + "\n")
else:
f.write(" ".join(txt) + f" @{line}" + "\n")
if __name__ == "__main__":
main()
@@ -0,0 +1,36 @@
import numpy as np
import sherpa_onnx
from xiaozhi.utils.file import get_model_file_path
class _SherpaOnnx:
def start(self):
self.keyword_spotter = sherpa_onnx.KeywordSpotter(
provider="cpu",
num_threads=1,
max_active_paths=4,
keywords_score=2.0,
keywords_threshold=0.2,
num_trailing_blanks=1,
keywords_file=get_model_file_path("keywords.txt"),
tokens=get_model_file_path("tokens.txt"),
encoder=get_model_file_path("encoder.onnx"),
decoder=get_model_file_path("decoder.onnx"),
joiner=get_model_file_path("joiner.onnx"),
)
self.stream = self.keyword_spotter.create_stream()
def kws(self, frames):
samples = np.frombuffer(frames, dtype=np.int16)
samples = samples.astype(np.float32) / 32768.0
self.stream.accept_waveform(16000, samples)
while self.keyword_spotter.is_ready(self.stream):
self.keyword_spotter.decode_stream(self.stream)
result = self.keyword_spotter.get_result(self.stream)
if result:
self.keyword_spotter.reset_stream(self.stream)
return result.lower()
SherpaOnnx = _SherpaOnnx()
@@ -0,0 +1,192 @@
import uuid
from typing import Any, Callable, ClassVar, Optional
import numpy as np
from config import APP_CONFIG
from xiaozhi.ref import get_xiaoai
class __GlobalStream:
def __init__(self):
self.readers = {}
self.on_output_data = None
def register_reader(self, reader):
if reader.id not in self.readers:
self.readers[reader.id] = reader
def unregister_reader(self, reader) -> None:
if reader.id in self.readers:
del self.readers[reader.id]
def input(self, data: bytes) -> None:
for key in self.readers:
self.readers[key].input(data)
def output(self, frames: bytes) -> None:
if self.on_output_data:
self.on_output_data(frames)
GlobalStream = __GlobalStream()
class MyStream:
def __init__(
self,
rate: int,
channels: int,
format: int,
input: bool = False,
output: bool = False,
frames_per_buffer: int = 1024,
start: bool = True,
) -> None:
self.id = uuid.uuid4()
self._rate = rate
self._channels = channels
self._format = format
self._frames_per_buffer = frames_per_buffer
self._is_input = input
self._is_output = output
self._is_active = False
self.input_bytes: list[int] = []
if start:
self.start_stream()
def close(self) -> None:
self.stop_stream()
def is_active(self) -> bool:
return self._is_active
def start_stream(self) -> None:
if not self._is_active:
self._is_active = True
if self._is_input:
GlobalStream.register_reader(self)
def stop_stream(self) -> None:
if self._is_active:
self._is_active = False
if self._is_input:
GlobalStream.unregister_reader(self)
self.input_bytes = []
def write(self, frames: bytes) -> None:
# 发送输出音频流到扬声器
if not self._is_output or not self._is_active:
return
GlobalStream.output(frames)
def input(self, data: bytes):
# 收到麦克风输入音频流
if not self._is_input or not self._is_active:
return
if len(data) > 0:
samples = np.frombuffer(data, dtype=np.int16)
# 小爱音箱录音音量较小,需要后期放大一下
samples = samples * APP_CONFIG["vad"]["boost"]
self.input_bytes.extend(samples.tobytes())
def read(self, num_frames=None, exception_on_overflow=False) -> bytes:
if num_frames is None:
data = bytes(self.input_bytes)
self.input_bytes = []
return data
num_frames = num_frames * 2
if (
not self._is_input
or not self._is_active
# 达不到预期长度时,返回空字节,等待下一次读取
or len(self.input_bytes) < num_frames
):
return bytes([])
data = bytes(self.input_bytes[:num_frames])
self.input_bytes = self.input_bytes[num_frames:]
return data
class MyAudio:
"""PyAudio 替代品,用于创建和管理音频流"""
Stream: ClassVar[type] = MyStream
@classmethod
def create(cls):
if get_xiaoai().mode != "xiaozhi":
return MyAudio()
else:
from pyaudio import PyAudio
return PyAudio()
@classmethod
def get_input_device_index(cls, audio):
if get_xiaoai().mode != "xiaozhi":
return 0
try:
device = audio.get_default_input_device_info()
return device["index"]
except Exception:
for i in range(audio.get_device_count()):
dev = audio.get_device_info_by_index(i)
if dev["maxInputChannels"] > 0:
return i
return 0
@classmethod
def get_output_device_index(cls, audio):
if get_xiaoai().mode != "xiaozhi":
return 0
try:
device = audio.get_default_output_device_info()
return device["index"]
except Exception:
for i in range(audio.get_device_count()):
dev = audio.get_device_info_by_index(i)
if dev["maxOutputChannels"] > 0:
return i
return 0
def __init__(self) -> None:
self._is_terminated = False
def open(
self,
rate: int,
channels: int,
format: int,
input: bool = False,
output: bool = False,
input_device_index: Optional[int] = None,
output_device_index: Optional[int] = None,
frames_per_buffer: int = 1024,
start: bool = True,
input_host_api_specific_stream_info: Optional[Any] = None,
output_host_api_specific_stream_info: Optional[Any] = None,
stream_callback: Optional[Callable] = None,
) -> MyStream:
if self._is_terminated:
raise RuntimeError("MyAudio instance has been terminated")
return MyStream(
rate=rate,
channels=channels,
format=format,
input=input,
output=output,
frames_per_buffer=frames_per_buffer,
start=start,
)
def terminate(self) -> None:
if not self._is_terminated:
self._is_terminated = True
@@ -0,0 +1,183 @@
import threading
import time
import numpy as np
from config import APP_CONFIG
from xiaozhi.event import EventManager
from xiaozhi.ref import set_vad
from xiaozhi.services.audio.stream import MyAudio
from xiaozhi.services.audio.vad.silero import VAD_MODEL
from xiaozhi.services.protocols.typing import AudioConfig
from xiaozhi.utils.base import get_env
class _VAD:
"""基于WebRTC VAD的语音活动检测器,用于检测用户打断"""
def __init__(self):
set_vad(self)
config = APP_CONFIG.get("vad", {})
# 参数设置
self.sample_rate = 16000
self.frame_size = 512
self.threshold = config.get("threshold", 0.01)
self.min_speech_duration = config.get("min_speech_duration", 250)
self.min_silence_duration = config.get("min_silence_duration", 500)
# 状态变量
self.paused = True
self.thread = None
self.speech_count = 0
self.silence_count = 0
# 创建独立的PyAudio实例和流,避免与主音频流冲突
self.audio = None
self.stream = None
# 暂存的语音片段
self.temp_frames = []
self.speech_buffer = []
self.target = None # 检测目标 speech/silence
def _reset_state(self):
"""重置状态"""
self.speech_count = 0
self.silence_count = 0
self.speech_buffer = []
self.temp_frames = []
def start(self):
"""启动VAD检测器"""
if not get_env("CLI"):
return
if self.thread and self.thread.is_alive():
return
self.paused = False
# 初始化PyAudio和流
self._initialize_audio_stream()
# 启动检测线程
self.thread = threading.Thread(target=self._detection_loop, daemon=True)
self.thread.start()
def pause(self):
"""暂停VAD检测"""
if not get_env("CLI"):
return
self.paused = True
self._reset_state()
self.stream.stop_stream()
def resume(self, target: str):
"""恢复VAD检测"""
if not get_env("CLI"):
return
self.paused = False
self.target = target
self.stream.start_stream()
def _handle_speech_frame(self, frames):
"""处理语音帧"""
self.speech_count += len(frames)
self.silence_count = 0
self.speech_buffer.extend(frames)
speech_bytes = bytes(self.speech_buffer)
if (
self.target == "speech"
and self.speech_count > self.min_speech_duration * self.sample_rate / 1000
):
self.pause()
EventManager.on_speech(speech_bytes)
def _handle_silence_frame(self, frames):
"""处理静音帧"""
self.silence_count += len(frames)
self.speech_count = 0
self.speech_buffer = []
if (
self.target == "silence"
and self.silence_count > self.min_silence_duration * self.sample_rate / 1000
):
self.pause()
EventManager.on_silence()
def _detect_speech(self, frames):
"""检测是否是语音"""
try:
audio_int16 = np.frombuffer(frames, dtype=np.int16)
audio_float32 = audio_int16.astype(np.float32) / 32768.0
speech_prob = VAD_MODEL(audio_float32, 16000).item()
is_speech = speech_prob >= self.threshold
return is_speech
except Exception:
return False
def _initialize_audio_stream(self):
"""初始化独立的音频流"""
try:
# 创建 PyAudio 实例
self.audio = MyAudio.create()
# 创建输入流
self.stream = self.audio.open(
format=AudioConfig.FORMAT,
channels=1,
rate=self.sample_rate,
input=True,
frames_per_buffer=self.frame_size,
start=True,
)
return True
except Exception:
return False
def _close_audio_stream(self):
"""关闭音频流"""
try:
if self.stream:
self.stream.stop_stream()
self.stream.close()
self.stream = None
if self.audio:
self.audio.terminate()
self.audio = None
except Exception:
pass
def _detection_loop(self):
"""VAD检测主循环"""
while True:
# 如果暂停或者音频流未初始化,则跳过
if self.paused or not self.stream:
time.sleep(0.1)
continue
# 读取缓冲区音频数据
frames = self.stream.read(self.frame_size)
if len(frames) != self.frame_size * 2:
time.sleep(0.01)
continue
# 检测是否是语音
is_speech = self._detect_speech(frames)
if is_speech:
self._handle_speech_frame(frames)
else:
self._handle_silence_frame(frames)
time.sleep(0.01)
VAD = _VAD()
@@ -0,0 +1,89 @@
import numpy as np
import onnxruntime as ort
from xiaozhi.utils.file import get_model_file_path
class OnnxWrapper:
def __init__(self, path):
opts = ort.SessionOptions()
opts.inter_op_num_threads = 1
opts.intra_op_num_threads = 1
self.session = ort.InferenceSession(
path, providers=["CPUExecutionProvider"], sess_options=opts
)
self.reset_states()
self.sample_rates = [8000, 16000]
def _validate_input(self, x, sr: int):
if len(x.shape) == 1:
x = np.expand_dims(x, 0)
if len(x.shape) > 2:
raise ValueError(
f"Too many dimensions for input audio chunk {len(x.shape)}"
)
if sr != 16000 and (sr % 16000 == 0):
step = sr // 16000
x = x[:, ::step]
sr = 16000
if sr not in self.sample_rates:
raise ValueError(
f"Supported sampling rates: {self.sample_rates} (or multiply of 16000)"
)
if sr / x.shape[1] > 31.25:
raise ValueError("Input audio chunk is too short")
return x, sr
def reset_states(self, batch_size=1):
self._state = np.zeros((2, batch_size, 128), dtype=np.float32)
self._context = np.zeros(0, dtype=np.float32)
self._last_sr = 0
self._last_batch_size = 0
def __call__(self, x, sr: int):
x, sr = self._validate_input(x, sr)
num_samples = 512 if sr == 16000 else 256
if x.shape[-1] != num_samples:
raise ValueError(
f"Provided number of samples is {x.shape[-1]} (Supported values: 256 for 8000 sample rate, 512 for 16000)"
)
batch_size = x.shape[0]
context_size = 64 if sr == 16000 else 32
if not self._last_batch_size:
self.reset_states(batch_size)
if (self._last_sr) and (self._last_sr != sr):
self.reset_states(batch_size)
if (self._last_batch_size) and (self._last_batch_size != batch_size):
self.reset_states(batch_size)
if not len(self._context):
self._context = np.zeros((batch_size, context_size), dtype=np.float32)
x = np.concatenate([self._context, x], axis=1)
if sr in [8000, 16000]:
ort_inputs = {
"input": x,
"state": self._state,
"sr": np.array(sr, dtype="int64"),
}
ort_outs = self.session.run(None, ort_inputs)
out, state = ort_outs
self._state = state
else:
raise ValueError()
self._context = x[..., -context_size:]
self._last_sr = sr
self._last_batch_size = batch_size
return out
VAD_MODEL = OnnxWrapper(
path=get_model_file_path("silero_vad.onnx"),
)
@@ -0,0 +1,49 @@
from abc import ABC, abstractmethod
from typing import Callable, Optional
class BaseDisplay(ABC):
"""显示接口的抽象基类"""
def __init__(self):
self.current_volume = 70 # 默认音量
@abstractmethod
def set_callbacks(
self,
press_callback: Optional[Callable] = None,
release_callback: Optional[Callable] = None,
status_callback: Optional[Callable] = None,
text_callback: Optional[Callable] = None,
emotion_callback: Optional[Callable] = None,
mode_callback: Optional[Callable] = None,
auto_callback: Optional[Callable] = None,
abort_callback: Optional[Callable] = None,
):
"""设置回调函数"""
pass
@abstractmethod
def update_status(self, status: str):
"""更新状态文本"""
pass
@abstractmethod
def update_text(self, text: str):
"""更新TTS文本"""
pass
@abstractmethod
def update_emotion(self, emotion: str):
"""更新表情"""
pass
@abstractmethod
def start(self):
"""启动显示"""
pass
@abstractmethod
def on_close(self):
"""关闭显示"""
pass
@@ -1,10 +1,9 @@
import queue
import threading
import time
import tkinter as tk
from tkinter import ttk
import queue
import logging
import time
from typing import Optional, Callable
from typing import Callable, Optional
from xiaozhi.services.display.base_display import BaseDisplay
@@ -13,14 +12,11 @@ class GuiDisplay(BaseDisplay):
def __init__(self):
super().__init__() # 调用父类初始化
"""创建 GUI 界面"""
# 初始化日志
self.logger = logging.getLogger("Display")
# 创建主窗口
self.root = tk.Tk()
self.root.title("小爱音箱接入小智 AI 演示")
self.root.geometry("520x360")
# 在窗口底部添加作者信息
self.author_label = ttk.Label(self.root, text="作者: https://del.wang")
self.author_label.pack(side=tk.BOTTOM, pady=5)
@@ -135,8 +131,8 @@ class GuiDisplay(BaseDisplay):
# 调用回调函数
if self.button_press_callback:
self.button_press_callback()
except Exception as e:
self.logger.error(f"按钮按下回调执行失败: {e}")
except Exception:
pass
def _on_manual_button_release(self, event):
"""手动模式按钮释放事件处理"""
@@ -147,67 +143,16 @@ class GuiDisplay(BaseDisplay):
# 调用回调函数
if self.button_release_callback:
self.button_release_callback()
except Exception as e:
self.logger.error(f"按钮释放回调执行失败: {e}")
def _on_auto_button_click(self):
"""自动模式按钮点击事件处理"""
try:
if self.auto_callback:
self.auto_callback()
except Exception as e:
self.logger.error(f"自动模式按钮回调执行失败: {e}")
except Exception:
pass
def _on_abort_button_click(self):
"""打断按钮点击事件处理"""
try:
if self.abort_callback:
self.abort_callback()
except Exception as e:
self.logger.error(f"打断按钮回调执行失败: {e}")
def _on_mode_button_click(self):
"""对话模式切换按钮点击事件"""
try:
# 检查是否可以切换模式(通过回调函数询问应用程序当前状态)
if self.mode_callback:
# 如果回调函数返回False,表示当前不能切换模式
if not self.mode_callback(not self.auto_mode):
return
# 切换模式
self.auto_mode = not self.auto_mode
# 更新按钮显示
if self.auto_mode:
# 切换到自动模式
self.update_mode_button_status("自动对话")
# 隐藏手动按钮,显示自动按钮
self.update_queue.put(lambda: self._switch_to_auto_mode())
else:
# 切换到手动模式
self.update_mode_button_status("手动对话")
# 隐藏自动按钮,显示手动按钮
self.update_queue.put(lambda: self._switch_to_manual_mode())
except Exception as e:
self.logger.error(f"模式切换按钮回调执行失败: {e}")
def _switch_to_auto_mode(self):
"""切换到自动模式的UI更新"""
self.manual_btn.pack_forget() # 移除手动按钮
self.auto_btn.pack(
side=tk.LEFT, padx=10, before=self.abort_btn
) # 显示自动按钮,放在打断按钮前面
def _switch_to_manual_mode(self):
"""切换到手动模式的UI更新"""
self.auto_btn.pack_forget() # 移除自动按钮
self.manual_btn.pack(
side=tk.LEFT, padx=10, before=self.abort_btn
) # 显示手动按钮,放在打断按钮前面
except Exception:
pass
def update_status(self, status: str):
"""更新状态文本"""
@@ -245,8 +190,8 @@ class GuiDisplay(BaseDisplay):
if emotion:
self.update_emotion(emotion)
except Exception as e:
self.logger.error(f"更新失败: {e}")
except Exception:
pass
time.sleep(0.1)
threading.Thread(target=update_loop, daemon=True).start()
@@ -262,23 +207,6 @@ class GuiDisplay(BaseDisplay):
# 启动更新线程
self.start_update_threads()
# 在主线程中运行主循环
self.logger.info("开始启动GUI主循环")
self.root.mainloop()
except Exception as e:
self.logger.error(f"GUI启动失败: {e}", exc_info=True)
# 尝试回退到CLI模式
print(f"GUI启动失败: {e},请尝试使用CLI模式")
def update_mode_button_status(self, text: str):
"""更新模式按钮状态"""
self.update_queue.put(lambda: self.mode_btn.config(text=text))
def update_button_status(self, text: str):
"""更新按钮状态 - 保留此方法以满足抽象基类要求"""
# 根据当前模式更新相应的按钮
if self.auto_mode:
self.update_queue.put(lambda: self.auto_btn.config(text=text))
else:
# 在手动模式下,不通过此方法更新按钮文本
# 因为按钮文本由按下/释放事件直接控制
except Exception:
pass
@@ -0,0 +1,38 @@
import time
from typing import Callable, Optional
from xiaozhi.services.display.base_display import BaseDisplay
class NoDisplay(BaseDisplay):
def set_callbacks(
self,
press_callback: Optional[Callable] = None,
release_callback: Optional[Callable] = None,
status_callback: Optional[Callable] = None,
text_callback: Optional[Callable] = None,
emotion_callback: Optional[Callable] = None,
mode_callback: Optional[Callable] = None,
auto_callback: Optional[Callable] = None,
abort_callback: Optional[Callable] = None,
):
pass
def update_status(self, status: str):
pass
def update_text(self, text: str):
pass
def update_emotion(self, emotion: str):
pass
def start_update_threads(self):
pass
def on_close(self):
pass
def start(self):
while True:
time.sleep(1)
@@ -1,6 +1,6 @@
import json
from xiaozhi.services.protocols.typing import AbortReason, ListeningMode
from xiaozhi.services.protocols.typing import ListeningMode
class Protocol:
@@ -38,12 +38,9 @@ class Protocol:
async def send_abort_speaking(self, reason):
"""发送中止语音的消息"""
message = {"session_id": self.session_id, "type": "abort"}
if reason == AbortReason.WAKE_WORD_DETECTED:
message["reason"] = "wake_word_detected"
message = {"session_id": self.session_id, "type": reason}
await self.send_text(json.dumps(message))
async def send_start_listening(self, mode):
"""发送开始监听的消息"""
mode_map = {
@@ -69,7 +66,7 @@ class Protocol:
message = {
"session_id": self.session_id,
"type": "iot",
"descriptors": json.loads(descriptors),
"descriptors": json.loads(descriptors),
}
await self.send_text(json.dumps(message))
@@ -6,7 +6,7 @@ class ListeningMode:
class AbortReason:
"""中止原因"""
NONE = "none"
ABORT = "abort"
WAKE_WORD_DETECTED = "wake_word_detected"
class DeviceState:
@@ -20,10 +20,10 @@ class EventType:
"""事件类型"""
SCHEDULE_EVENT = "schedule_event"
AUDIO_INPUT_READY_EVENT = "audio_input_ready_event"
AUDIO_OUTPUT_READY_EVENT = "audio_output_ready_event"
class AudioConfig:
"""音频配置"""
FORMAT = 8
SAMPLE_RATE = 24000
CHANNELS = 1
FRAME_DURATION = 60 # ms
@@ -1,14 +1,10 @@
import asyncio
import json
import logging
import websockets
from xiaozhi.services.protocols.protocol import Protocol
from xiaozhi.utils.config_manager import ConfigManager
logger = logging.getLogger("WebsocketProtocol")
from xiaozhi.utils.config import ConfigManager
class WebsocketProtocol(Protocol):
@@ -41,17 +37,10 @@ class WebsocketProtocol(Protocol):
"Client-Id": self.CLIENT_ID,
}
# 建立WebSocket连接 (兼容不同Python版本的写法)
try:
# 新的写法 (在Python 3.11+版本中)
self.websocket = await websockets.connect(
uri=self.WEBSOCKET_URL, additional_headers=headers
)
except TypeError:
# 旧的写法 (在较早的Python版本中)
self.websocket = await websockets.connect(
self.WEBSOCKET_URL, extra_headers=headers
)
# 建立WebSocket连接
self.websocket = await websockets.connect(
uri=self.WEBSOCKET_URL, additional_headers=headers
)
# 启动消息处理循环
asyncio.create_task(self._message_handler())
@@ -74,16 +63,13 @@ class WebsocketProtocol(Protocol):
try:
await asyncio.wait_for(self.hello_received.wait(), timeout=10.0)
self.connected = True
logger.info("已连接到WebSocket服务器")
return True
except asyncio.TimeoutError:
logger.error("等待服务器hello响应超时")
if self.on_network_error:
self.on_network_error("等待响应超时")
return False
except Exception as e:
logger.error(f"WebSocket连接失败: {e}")
if self.on_network_error:
self.on_network_error(f"无法连接服务: {str(e)}")
return False
@@ -97,27 +83,21 @@ class WebsocketProtocol(Protocol):
data = json.loads(message)
msg_type = data.get("type")
if msg_type == "hello":
# 处理服务器 hello 消息
await self._handle_server_hello(data)
else:
if self.on_incoming_json:
self.on_incoming_json(data)
except json.JSONDecodeError as e:
logger.error(f"无效的JSON消息: {message}, 错误: {e}")
elif self.on_incoming_audio: # 使用 elif 更清晰
except json.JSONDecodeError:
pass
elif self.on_incoming_audio:
self.on_incoming_audio(message)
except websockets.ConnectionClosed:
logger.info("WebSocket连接已关闭")
self.connected = False
if self.on_audio_channel_closed:
# 使用 schedule 确保回调在主线程中执行
await self.on_audio_channel_closed()
except Exception as e:
logger.error(f"消息处理错误: {e}")
self.connected = False
if self.on_network_error:
# 使用 schedule 确保错误处理在主线程中执行
self.on_network_error(f"连接错误: {str(e)}")
async def send_audio(self, data: bytes):
@@ -128,7 +108,6 @@ class WebsocketProtocol(Protocol):
try:
await self.websocket.send(data)
except Exception as e:
logger.error(f"发送音频数据失败: {e}")
if self.on_network_error:
self.on_network_error(f"发送音频失败: {str(e)}")
@@ -169,7 +148,6 @@ class WebsocketProtocol(Protocol):
# 验证传输方式
transport = data.get("transport")
if not transport or transport != "websocket":
logger.error(f"不支持的传输方式: {transport}")
return
# 获取音频参数
@@ -179,13 +157,6 @@ class WebsocketProtocol(Protocol):
sample_rate = audio_params.get("sample_rate")
if sample_rate:
self.server_sample_rate = sample_rate
# 如果服务器采样率与本地不同,记录警告
if sample_rate != self.server_sample_rate:
logger.warning(
f"服务器的音频采样率 {sample_rate} "
f"与设备输出的采样率 {self.server_sample_rate} 不一致,"
"重采样后可能会失真"
)
# 设置 hello 接收事件
self.hello_received.set()
@@ -194,10 +165,7 @@ class WebsocketProtocol(Protocol):
if self.on_audio_channel_opened:
await self.on_audio_channel_opened()
logger.info("成功处理服务器 hello 消息")
except Exception as e:
logger.error(f"处理服务器 hello 消息时出错: {e}")
if self.on_network_error:
self.on_network_error(f"处理服务器响应失败: {str(e)}")
@@ -210,5 +178,5 @@ class WebsocketProtocol(Protocol):
self.connected = False
if self.on_audio_channel_closed:
await self.on_audio_channel_closed()
except Exception as e:
logger.error(f"关闭WebSocket连接失败: {e}")
except Exception:
pass
@@ -0,0 +1,186 @@
from typing import Literal
from xiaozhi.ref import get_xiaoai, set_speaker
from xiaozhi.utils.base import json_decode, json_encode
class CommandResult:
def __init__(self, stdout: str, stderr: str, exit_code: int):
self.stdout = stdout
self.stderr = stderr
self.exit_code = exit_code
class SpeakerManager:
status: Literal["playing", "paused", "idle"] = "idle"
def __init__(self):
set_speaker(self)
async def get_playing(self, sync=False):
"""获取播放状态"""
if sync:
# 同步远端最新状态
res = await self.run_shell("mphelper mute_stat")
if "1" in res.stdout:
self.status = "playing"
elif "2" in res.stdout:
self.status = "paused"
return self.status
async def set_playing(self, playing=True):
"""播放/暂停"""
command = "mphelper play" if playing else "mphelper pause"
res = await self.run_shell(command)
return '"code": 0' in res.stdout
async def play(
self,
text=None,
url=None,
buffer=None,
blocking=True,
timeout=10 * 60 * 1000,
):
"""
播放文字、音频链接、音频流
参数:
text: 文字内容
url: 音频链接
buffer: 音频流
timeout: 超时时长(毫秒),默认10分钟
blocking: 是否阻塞运行(仅对播放文字、音频链接有效)
"""
if buffer is not None:
return get_xiaoai().on_output_data(buffer)
if blocking:
command = (
f"miplayer -f '{url}'"
if url
else f"/usr/sbin/tts_play.sh '{text.replace("'", "'\\''") or '你好'}'"
)
res = await self.run_shell(command, timeout=timeout)
return res.exit_code == 0
if url:
data = json_encode({"url": url, "type": 1})
command = f"ubus call mediaplayer player_play_url '{data}'"
else:
data = json_encode({"text": text or "你好", "save": 0})
command = f"ubus call mibrain text_to_speech '{data}'"
res = await self.run_shell(command, timeout=timeout)
return '"code": 0' in res.stdout if res else False
async def wake_up(self, awake=True, silent=True):
"""
(取消)唤醒小爱
参数:
awake: 是否唤醒
silent: 是否静默唤醒
"""
if awake:
if silent:
command = 'ubus call pnshelper event_notify \'{"src":1,"event":0}\''
else:
command = 'ubus call pnshelper event_notify \'{"src":0,"event":0}\''
else:
command = """
ubus call pnshelper event_notify '{"src":3, "event":7}'
sleep 0.1
ubus call pnshelper event_notify '{"src":3, "event":8}'
"""
res = await self.run_shell(command)
return '"code": 0' in res.stdout
async def ask_xiaoai(self, text: str, silent=False):
"""
把文字指令交给原来的小爱执行
参数:
text: 文字指令
silent: 是否静默执行
"""
data = {"nlp": 1, "nlp_text": text}
if not silent:
data["tts"] = 1
command = f"ubus call mibrain ai_service '{json_encode(data)}'"
res = await self.run_shell(command)
return '"code": 0' in res.stdout
async def abort_xiaoai(self):
"""
中断原来小爱的运行
注意:重启需要大约 1-2s 的时间,在此期间无法使用小爱音箱自带的 TTS 服务
"""
res = await self.run_shell("/etc/init.d/mico_aivs_lab restart >/dev/null 2>&1")
return res.exit_code == 0
async def get_boot(self):
"""获取启动分区"""
res = await self.run_shell("echo $(fw_env -g boot_part)")
return res.stdout.strip()
async def set_boot(self, boot_part: Literal["boot0", "boot1"]):
"""设置启动分区"""
command = f"fw_env -s boot_part {boot_part} >/dev/null 2>&1 && echo $(fw_env -g boot_part)"
res = await self.run_shell(command)
return boot_part in res.stdout
async def get_device(self):
"""获取设备型号、序列号信息"""
res = await self.run_shell("echo $(micocfg_model) $(micocfg_sn)")
info = res.stdout.strip().split(" ")
return {
"model": info[0] if len(info) > 0 else "unknown",
"sn": info[1] if len(info) > 1 else "unknown",
}
async def get_mic(self):
"""获取麦克风状态"""
res = await self.run_shell("[ ! -f /tmp/mipns/mute ] && echo on || echo off")
status = "off"
if "on" in res.stdout:
status = "on"
return status
async def set_mic(self, on=True):
"""打开/关闭麦克风"""
if on:
command = (
'ubus -t1 -S call pnshelper event_notify \'{"src":3, "event":7}\' 2>&1'
)
else:
command = (
'ubus -t1 -S call pnshelper event_notify \'{"src":3, "event":8}\' 2>&1'
)
res = await self.run_shell(command)
return '"code":0' in res.stdout
async def run_shell(self, script: str, timeout=10000):
"""
执行脚本
参数:
script: 脚本内容
timeout: 超时时间(毫秒)
"""
res = "unknown"
try:
res = await get_xiaoai().run_shell(script, timeout=timeout)
data = json_decode(res)
if data:
return CommandResult(
data.get("stdout", ""),
data.get("stderr", ""),
data.get("exit_code", 0),
)
except Exception:
return CommandResult("error", res, -1)
+33
View File
@@ -0,0 +1,33 @@
import json
import os
import random
def get_env(key: str, default_value: str | None = None):
return os.environ.get(key, default_value)
def to_set(data):
if isinstance(data, list):
return list(set(data))
return data
def pick_one(data: list):
if len(data) == 0:
return None
return data[random.randint(0, len(data) - 1)]
def json_encode(obj, pretty=False):
try:
return json.dumps(obj, ensure_ascii=False, indent=4 if pretty else None)
except Exception as _:
return None
def json_decode(text):
try:
return json.loads(text)
except Exception:
return None
@@ -1,14 +1,13 @@
import json
import logging
import os
from pathlib import Path
from typing import Dict, Any, Optional
import threading
import requests
import re
import socket
import threading
import uuid
from typing import Any, Optional
logger = logging.getLogger("ConfigManager")
import requests
from config import APP_CONFIG
from xiaozhi.utils.file import read_file, write_file
class ConfigManager:
@@ -16,19 +15,6 @@ class ConfigManager:
_instance = None
_lock = threading.Lock()
CONFIG_FILE = Path(os.getcwd()) / "xiaozhi.json"
# 默认配置
DEFAULT_CONFIG = {
"CLIENT_ID": None,
"DEVICE_ID": None,
"NETWORK": {
"OTA_VERSION_URL": "https://api.tenclass.net/xiaozhi/ota/",
"WEBSOCKET_URL": "wss://api.tenclass.net/xiaozhi/v1/",
"WEBSOCKET_ACCESS_TOKEN": "test-token",
},
"MQTT_INFO": None,
}
def __new__(cls):
"""确保单例模式"""
@@ -38,56 +24,22 @@ class ConfigManager:
def __init__(self):
"""初始化配置管理器"""
self.logger = logger
if hasattr(self, "_initialized"):
return
self._initialized = True
# 加载配置
self._config = self._load_config()
self._config = {
"CLIENT_ID": None,
"DEVICE_ID": APP_CONFIG["xiaozhi"]["DEVICE_ID"],
"NETWORK": APP_CONFIG.get("xiaozhi"),
"MQTT_INFO": None,
}
self._initialize_client_id()
self._initialize_device_id()
self._initialize_mqtt_info()
def _load_config(self) -> Dict[str, Any]:
"""加载配置文件,如果不存在则创建"""
try:
if self.CONFIG_FILE.exists():
config = json.loads(self.CONFIG_FILE.read_text(encoding="utf-8"))
return self._merge_configs(self.DEFAULT_CONFIG, config)
else:
self._save_config(self.DEFAULT_CONFIG)
return self.DEFAULT_CONFIG.copy()
except Exception as e:
logger.error(f"Error loading config: {e}")
return self.DEFAULT_CONFIG.copy()
def _save_config(self, config: dict) -> bool:
"""保存配置到文件"""
try:
self.CONFIG_FILE.write_text(
json.dumps(config, indent=2, ensure_ascii=False), encoding="utf-8"
)
return True
except Exception as e:
logger.error(f"Error saving config: {e}")
return False
@staticmethod
def _merge_configs(default: dict, custom: dict) -> dict:
"""递归合并配置字典"""
result = default.copy()
for key, value in custom.items():
if (
key in result
and isinstance(result[key], dict)
and isinstance(value, dict)
):
result[key] = ConfigManager._merge_configs(result[key], value)
else:
result[key] = value
return result
def get_client_id(self) -> str:
"""获取客户端ID"""
return self._config["CLIENT_ID"]
@@ -122,11 +74,23 @@ class ConfigManager:
for part in parts:
current = current.setdefault(part, {})
current[last] = value
return self._save_config(self._config)
except Exception as e:
logger.error(f"Error updating config {path}: {e}")
return True
except Exception:
return False
def update_config_file(self, path: str, value: Any):
"""
更新 config.py 文件中的特定配置项
"""
write_file(
"config.py",
re.sub(
r'"{}"\s*:\s*"[^"]*"'.format(path),
f'"{path}": "{value}"',
read_file("config.py"),
),
)
@classmethod
def instance(cls):
"""获取配置管理器实例(线程安全)"""
@@ -156,101 +120,101 @@ class ConfigManager:
"""确保存在客户端ID"""
if not self._config["CLIENT_ID"]:
client_id = self.generate_uuid()
success = self.update_config("CLIENT_ID", client_id)
if success:
logger.info(f"Generated new CLIENT_ID: {client_id}")
else:
logger.error("Failed to save new CLIENT_ID")
self.update_config("CLIENT_ID", client_id)
def _initialize_device_id(self):
"""确保存在设备ID"""
if self._config["DEVICE_ID"]:
# 检查设备 ID 是否符合 MAC 地址格式(如 a6:85:b4:9c:09:66)
mac_pattern = re.compile(r"^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$")
if not mac_pattern.match(self._config["DEVICE_ID"]):
self._config["DEVICE_ID"] = None
if not self._config["DEVICE_ID"]:
try:
device_hash = self.get_mac_address()
success = self.update_config("DEVICE_ID", device_hash)
if success:
logger.info(f"Generated new DEVICE_ID: {device_hash}")
else:
logger.error("Failed to save new DEVICE_ID")
except Exception as e:
logger.error(f"Error generating DEVICE_ID: {e}")
self.update_config("DEVICE_ID", device_hash)
self.update_config_file("DEVICE_ID", device_hash)
except Exception:
pass
def refresh_mqtt_info(self):
"""刷新 MQTT 信息"""
if not self._config["MQTT_INFO"]:
self._initialize_mqtt_info()
def _initialize_mqtt_info(self):
try:
mqtt_info = self._get_ota_version()
if mqtt_info:
self.update_config("MQTT_INFO", mqtt_info)
self.logger.info("MQTT信息已成功更新")
return mqtt_info
else:
self.logger.warning("获取MQTT信息失败,使用已保存的配置")
return self.get_config("MQTT_INFO")
except Exception as e:
self.logger.error(f"初始化MQTT信息失败: {e}")
except Exception:
return self.get_config("MQTT_INFO")
def _get_ota_version(self):
"""获取OTA服务器的MQTT信息"""
MAC_ADDR = self.get_device_id()
OTA_VERSION_URL = self.get_config("NETWORK.OTA_VERSION_URL")
headers = {"Device-Id": MAC_ADDR, "Content-Type": "application/json"}
OTA_URL = self.get_config("NETWORK.OTA_URL")
headers = {
"Activation-Version": "1",
"Device-Id": MAC_ADDR,
"Content-Type": "application/json",
"Accept-Language": "zh-CN",
}
# 构建设备信息payload
# 构建设备信息 payload
payload = {
"flash_size": 16777216, # 闪存大小 (16MB)
"minimum_free_heap_size": 8318916, # 最小可用堆内存
"mac_address": MAC_ADDR, # 设备MAC地址
"chip_model_name": "esp32s3", # 芯片型号
"chip_info": {"model": 9, "cores": 2, "revision": 2, "features": 18},
"application": {
"name": "xiaozhi",
"version": "1.1.2",
"idf_version": "v5.3.2-dirty",
},
"partition_table": [],
"ota": {"label": "factory"},
"mac_address": MAC_ADDR,
"board": {
"type": "bread-compact-wifi",
"type": "lc-esp32-s3",
"name": "立创ESP32-S3开发板",
"features": ["wifi", "ble", "psram", "octal_flash"],
"ip": self.get_local_ip(),
"mac": MAC_ADDR,
},
"application": {
"name": "xiaozhi",
"version": "1.6.0",
"compile_time": "2025-4-16T12:00:00Z",
"idf_version": "v5.3.2",
},
"psram_size": 8388608, # 8MB PSRAM
"minimum_free_heap_size": 7265024, # 最小可用堆内存
"chip_model_name": "esp32s3", # 芯片型号
"chip_info": {
"model": 9, # ESP32-S3
"cores": 2,
"revision": 0, # 芯片版本修订
"features": 20, # WiFi + BLE + PSRAM
},
"partition_table": [],
"ota": {"label": "factory"},
}
try:
# 发送请求到OTA服务器
response = requests.post(
OTA_VERSION_URL,
OTA_URL,
headers=headers,
json=payload,
timeout=10,
timeout=10,
)
# 检查HTTP状态码
if response.status_code != 200:
self.logger.error(f"OTA服务器错误: HTTP {response.status_code}")
raise ValueError(f"OTA服务器返回错误状态码: {response.status_code}")
# 解析JSON数据
response_data = response.json()
# 调试信息:打印完整的OTA响应
self.logger.debug(
f"OTA服务器返回数据: {json.dumps(response_data, indent=4, ensure_ascii=False)}"
)
# 确保"mqtt"信息存在
if "mqtt" in response_data:
self.logger.info(f"MQTT服务器信息已更新")
return response_data["mqtt"]
else:
self.logger.error("OTA服务器返回的数据无效: MQTT信息缺失")
raise ValueError("OTA服务器返回的数据无效,请检查服务器状态或MAC地址!")
except requests.Timeout:
self.logger.error("OTA请求超时,请检查网络或服务器状态")
raise ValueError("OTA请求超时!请稍后重试。")
except requests.RequestException as e:
self.logger.error(f"OTA请求失败: {e}")
except requests.RequestException:
raise ValueError("无法连接到OTA服务器,请检查网络连接!")
+16
View File
@@ -0,0 +1,16 @@
import os
def get_model_file_path(file_name: str):
current_dir = os.path.dirname(os.path.abspath(__file__))
return os.path.abspath(os.path.join(current_dir, "../models", file_name))
def read_file(file_path: str):
with open(file_path, "r", encoding="utf-8") as f:
return f.read()
def write_file(file_path: str, content: str):
with open(file_path, "w", encoding="utf-8") as f:
f.write(content)
+122
View File
@@ -0,0 +1,122 @@
import argparse
import asyncio
import threading
import numpy as np
import open_xiaoai_server
from config import APP_CONFIG
from xiaozhi.event import EventManager
from xiaozhi.ref import get_speaker, set_xiaoai
from xiaozhi.services.audio.stream import GlobalStream
from xiaozhi.services.speaker import SpeakerManager
from xiaozhi.utils.base import json_decode
ASCII_BANNER = """
▄▖ ▖▖▘ ▄▖▄▖
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
v1.0.0 by: https://del.wang
"""
class XiaoAI:
mode = "xiaoai"
speaker = SpeakerManager()
async_loop: asyncio.AbstractEventLoop = None
@classmethod
def setup_mode(cls):
set_xiaoai(cls)
parser = argparse.ArgumentParser(
description="小爱音箱接入小智 AI | by: https://del.wang"
)
parser.add_argument(
"--mode",
type=str,
choices=["xiaoai", "xiaozhi"],
default="xiaoai",
help="运行模式:【xiaoai】使用小爱音箱的输入输出音频(默认)、【xiaozhi】使用本地电脑的输入输出音频",
)
args = parser.parse_args()
if args.mode == "xiaozhi":
cls.mode = "xiaozhi"
@classmethod
def on_input_data(cls, data: bytes):
audio_array = np.frombuffer(data, dtype=np.uint16)
GlobalStream.input(audio_array.tobytes())
@classmethod
def on_output_data(cls, data: bytes):
async def on_output_data_async(data: bytes):
return await open_xiaoai_server.on_output_data(data)
asyncio.run_coroutine_threadsafe(
on_output_data_async(data),
cls.async_loop,
)
@classmethod
async def run_shell(cls, script: str, timeout: float = 10 * 1000):
return await open_xiaoai_server.run_shell(script, timeout)
@classmethod
async def on_event(cls, event: str):
event_json = json_decode(event) or {}
event_data = event_json.get("data", {})
event_type = event_json.get("event")
async def on_wakeup(text, source):
before_wakeup = APP_CONFIG["wakeup"]["before_wakeup"]
wakeup = await before_wakeup(get_speaker(), text, source)
if wakeup:
EventManager.on_wakeup()
if not event_json.get("event"):
return
if event_type == "instruction" and event_data.get("NewLine"):
line = json_decode(event_data.get("NewLine"))
if (
line
and line.get("header", {}).get("namespace") == "SpeechRecognizer"
and line.get("header", {}).get("name") == "RecognizeResult"
):
text = line.get("payload", {}).get("results")[0].get("text")
if not text and not line.get("payload", {}).get("is_vad_begin"):
print(f"🔥 唤醒小爱: {line}")
EventManager.on_interrupt()
elif text and line.get("payload", {}).get("is_final"):
print(f"🔥 收到指令: {text}")
await on_wakeup(text, "xiaoai")
elif event_type == "playing":
get_speaker().status = event_data.lower()
@classmethod
def __init_background_event_loop(cls):
def run_event_loop():
cls.async_loop = asyncio.new_event_loop()
asyncio.set_event_loop(cls.async_loop)
cls.async_loop.run_forever()
thread = threading.Thread(target=run_event_loop, daemon=True)
thread.start()
@classmethod
def __on_event(cls, event: str):
asyncio.run_coroutine_threadsafe(
cls.on_event(event),
cls.async_loop,
)
@classmethod
async def init_xiaoai(cls):
GlobalStream.on_output_data = cls.on_output_data
open_xiaoai_server.register_fn("on_input_data", cls.on_input_data)
open_xiaoai_server.register_fn("on_event", cls.__on_event)
cls.__init_background_event_loop()
print(ASCII_BANNER)
await open_xiaoai_server.start_server()
@@ -1,10 +1,13 @@
import asyncio
import json
import logging
import re
import threading
import time
from xiaozhi.services.display import gui_display
from xiaozhi.event import EventManager
from xiaozhi.ref import set_xiaozhi
from xiaozhi.services.audio.kws import KWS
from xiaozhi.services.audio.vad import VAD
from xiaozhi.services.protocols.typing import (
AbortReason,
AudioConfig,
@@ -13,11 +16,9 @@ from xiaozhi.services.protocols.typing import (
ListeningMode,
)
from xiaozhi.services.protocols.websocket_protocol import WebsocketProtocol
from xiaozhi.utils.config_manager import ConfigManager
from xiaozhi.xiaoai import XiaoAi
# 配置日志
logger = logging.getLogger("XiaoZhi")
from xiaozhi.utils.base import get_env
from xiaozhi.utils.config import ConfigManager
from xiaozhi.xiaoai import XiaoAI
class XiaoZhi:
@@ -45,8 +46,6 @@ class XiaoZhi:
# 状态变量
self.device_state = DeviceState.IDLE
self.voice_detected = False
self.keep_listening = False
self.aborted = False
self.current_text = ""
self.current_emotion = "neutral"
@@ -72,11 +71,11 @@ class XiaoZhi:
self.events = {
EventType.SCHEDULE_EVENT: threading.Event(),
EventType.AUDIO_INPUT_READY_EVENT: threading.Event(),
EventType.AUDIO_OUTPUT_READY_EVENT: threading.Event(),
}
# 创建显示界面
self.display = None
set_xiaozhi(self)
def run(self):
self.protocol = WebsocketProtocol()
@@ -90,7 +89,7 @@ class XiaoZhi:
time.sleep(0.1)
# 初始化应用程序(移除自动连接)
asyncio.run_coroutine_threadsafe(XiaoAi.init_xiaoai(), self.loop)
asyncio.run_coroutine_threadsafe(XiaoAI.init_xiaoai(), self.loop)
asyncio.run_coroutine_threadsafe(self._initialize_without_connect(), self.loop)
# 启动主循环线程
@@ -98,6 +97,9 @@ class XiaoZhi:
main_loop_thread.daemon = True
main_loop_thread.start()
VAD.start()
KWS.start()
# 启动 GUI
self._initialize_display()
self.display.start()
@@ -109,10 +111,6 @@ class XiaoZhi:
async def _initialize_without_connect(self):
"""初始化应用程序组件(不建立连接)"""
logger.info("正在初始化应用程序...")
# 设置设备状态为待命
self.set_device_state(DeviceState.IDLE)
# 初始化音频编解码器
self._initialize_audio()
@@ -124,7 +122,8 @@ class XiaoZhi:
self.protocol.on_audio_channel_opened = self._on_audio_channel_opened
self.protocol.on_audio_channel_closed = self._on_audio_channel_closed
logger.info("应用程序初始化完成")
# 设置设备状态为待命
self.set_device_state(DeviceState.IDLE)
def _initialize_audio(self):
"""初始化音频设备和编解码器"""
@@ -132,14 +131,19 @@ class XiaoZhi:
from xiaozhi.services.audio.codec import AudioCodec
self.audio_codec = AudioCodec()
logger.info("音频编解码器初始化成功")
except Exception as e:
logger.error(f"初始化音频设备失败: {e}")
self.alert("错误", f"初始化音频设备失败: {e}")
def _initialize_display(self):
"""初始化显示界面"""
self.display = gui_display.GuiDisplay()
if get_env("CLI"):
from xiaozhi.services.display import no_display
self.display = no_display.NoDisplay()
else:
from xiaozhi.services.display import gui_display
self.display = gui_display.GuiDisplay()
# 设置回调函数
self.display.set_callbacks(
@@ -155,7 +159,6 @@ class XiaoZhi:
def _main_loop(self):
"""应用程序主循环"""
logger.info("主循环已启动")
self.running = True
while self.running:
@@ -166,12 +169,9 @@ class XiaoZhi:
if event_type == EventType.AUDIO_INPUT_READY_EVENT:
self._handle_input_audio()
elif event_type == EventType.AUDIO_OUTPUT_READY_EVENT:
self._handle_output_audio()
elif event_type == EventType.SCHEDULE_EVENT:
self._process_scheduled_tasks()
# 短暂休眠以避免CPU占用过高
time.sleep(0.01)
def _process_scheduled_tasks(self):
@@ -183,8 +183,8 @@ class XiaoZhi:
for task in tasks:
try:
task()
except Exception as e:
logger.error(f"执行调度任务时出错: {e}")
except Exception:
pass
def schedule(self, callback):
"""调度任务到主循环"""
@@ -208,19 +208,10 @@ class XiaoZhi:
self.protocol.send_audio(encoded_data), self.loop
)
def _handle_output_audio(self):
"""处理音频输出"""
if self.device_state != DeviceState.SPEAKING:
return
self.audio_codec.play_audio()
def _on_network_error(self, message):
"""网络错误回调"""
self.keep_listening = False
self.set_device_state(DeviceState.IDLE)
if self.device_state != DeviceState.CONNECTING:
logger.info("检测到连接断开")
self.set_device_state(DeviceState.IDLE)
# 关闭现有连接
@@ -232,7 +223,6 @@ class XiaoZhi:
def _attempt_reconnect(self):
"""尝试重新连接服务器"""
if self.device_state != DeviceState.CONNECTING:
logger.info("检测到连接断开,尝试重新连接...")
self.set_device_state(DeviceState.CONNECTING)
# 关闭现有连接
@@ -263,16 +253,13 @@ class XiaoZhi:
max_retries = 3
while retry_count < max_retries:
logger.info(f"尝试重新连接 (尝试 {retry_count + 1}/{max_retries})...")
if await self.protocol.connect():
logger.info("重新连接成功")
self.set_device_state(DeviceState.IDLE)
return True
retry_count += 1
await asyncio.sleep(2) # 等待2秒后重试
logger.error(f"重新连接失败,已尝试 {max_retries}")
self.schedule(lambda: self.alert("连接错误", "无法重新连接到服务器"))
self.set_device_state(DeviceState.IDLE)
return False
@@ -281,7 +268,6 @@ class XiaoZhi:
"""接收音频数据回调"""
if self.device_state == DeviceState.SPEAKING:
self.audio_codec.write_audio(data)
self.events[EventType.AUDIO_OUTPUT_READY_EVENT].set()
def _on_incoming_json(self, json_data):
"""接收JSON数据回调"""
@@ -303,35 +289,33 @@ class XiaoZhi:
self._handle_stt_message(data)
elif msg_type == "llm":
self._handle_llm_message(data)
else:
logger.warning(f"收到未知类型的消息: {msg_type}")
except Exception as e:
logger.error(f"处理JSON消息时出错: {e}")
except Exception:
pass
def _handle_tts_message(self, data):
"""处理TTS消息"""
state = data.get("state", "")
if state == "start":
EventManager.on_tts_start()
self.schedule(lambda: self._handle_tts_start())
elif state == "stop":
EventManager.on_tts_end()
self.schedule(lambda: self._handle_tts_stop())
elif state == "sentence_start":
text = data.get("text", "")
if text:
logger.info(f"<< {text}")
self.schedule(lambda: self.set_chat_message("assistant", text))
print(f"🤖 小智:{text}")
# 检查是否包含验证码信息
if "请登录到控制面板添加设备,输入验证码" in text:
self.schedule(lambda: self._handle_verification_code(text))
verification_code = re.findall(r"验证码.*\d+", text)
if verification_code:
self.config.update_config_file(
"VERIFICATION_CODE", verification_code[0]
)
self.schedule(lambda: self.set_chat_message("assistant", text))
def _handle_tts_start(self):
"""处理TTS开始事件"""
self.aborted = False
# 清空可能存在的旧音频数据
self.audio_codec.clear_audio_queue()
if (
self.device_state == DeviceState.IDLE
or self.device_state == DeviceState.LISTENING
@@ -340,30 +324,13 @@ class XiaoZhi:
def _handle_tts_stop(self):
"""处理TTS停止事件"""
if self.device_state == DeviceState.SPEAKING:
# 给音频播放一个缓冲时间,确保所有音频都播放完毕
def delayed_state_change():
# 等待音频队列清空
self.audio_codec.wait_for_audio_complete()
# 状态转换
if self.keep_listening:
asyncio.run_coroutine_threadsafe(
self.protocol.send_start_listening(ListeningMode.AUTO_STOP),
self.loop,
)
self.set_device_state(DeviceState.LISTENING)
else:
self.set_device_state(DeviceState.IDLE)
# 安排延迟执行
threading.Thread(target=delayed_state_change, daemon=True).start()
pass
def _handle_stt_message(self, data):
"""处理STT消息"""
text = data.get("text", "")
if text:
logger.info(f">> {text}")
print(f"💬 我说:{text}")
self.schedule(lambda: self.set_chat_message("user", text))
def _handle_llm_message(self, data):
@@ -374,26 +341,19 @@ class XiaoZhi:
async def _on_audio_channel_opened(self):
"""音频通道打开回调"""
logger.info("音频通道已打开")
self.schedule(lambda: self._start_audio_streams())
def _start_audio_streams(self):
"""启动音频流"""
try:
# 确保流已关闭后再重新打开
if (
self.audio_codec.input_stream
and self.audio_codec.input_stream.is_active()
):
if self.audio_codec.input_stream.is_active():
self.audio_codec.input_stream.stop_stream()
# 重新打开流
self.audio_codec.input_stream.start_stream()
if (
self.audio_codec.output_stream
and self.audio_codec.output_stream.is_active()
):
if self.audio_codec.output_stream.is_active():
self.audio_codec.output_stream.stop_stream()
# 重新打开流
@@ -403,142 +363,65 @@ class XiaoZhi:
threading.Thread(
target=self._audio_input_event_trigger, daemon=True
).start()
threading.Thread(
target=self._audio_output_event_trigger, daemon=True
).start()
logger.info("音频流已启动")
except Exception as e:
logger.error(f"启动音频流失败: {e}")
except Exception:
pass
def _audio_input_event_trigger(self):
"""音频输入事件触发器"""
while self.running:
try:
if (
self.audio_codec.input_stream
and self.audio_codec.input_stream.is_active()
):
if self.audio_codec.input_stream.is_active():
self.events[EventType.AUDIO_INPUT_READY_EVENT].set()
except OSError as e:
logger.error(f"音频输入流错误: {e}")
# 如果流已关闭,尝试重新打开或者退出循环
if "Stream not open" in str(e):
break
except Exception as e:
logger.error(f"音频输入事件触发器错误: {e}")
except Exception:
pass
time.sleep(AudioConfig.FRAME_DURATION / 1000) # 按帧时长触发
def _audio_output_event_trigger(self):
"""音频输出事件触发器"""
while (
self.running
and self.audio_codec.output_stream
and self.audio_codec.output_stream.is_active()
):
# 当队列中有数据时才触发事件
if (
not self.audio_codec.audio_decode_queue.empty()
): # 修改为使用 audio_codec 的队列
self.events[EventType.AUDIO_OUTPUT_READY_EVENT].set()
time.sleep(0.02) # 稍微延长检查间隔
async def _on_audio_channel_closed(self):
"""音频通道关闭回调"""
logger.info("音频通道已关闭")
self.set_device_state(DeviceState.IDLE)
self.keep_listening = False
self.schedule(lambda: self._stop_audio_streams())
def _stop_audio_streams(self):
"""停止音频流"""
try:
if (
self.audio_codec.input_stream
and self.audio_codec.input_stream.is_active()
):
self.audio_codec.input_stream.stop_stream()
if (
self.audio_codec.output_stream
and self.audio_codec.output_stream.is_active()
):
self.audio_codec.output_stream.stop_stream()
logger.info("音频流已停止")
except Exception as e:
logger.error(f"停止音频流失败: {e}")
self.audio_codec.stop_streams()
def set_device_state(self, state):
"""设置设备状态"""
if self.device_state == state:
return
old_state = self.device_state
# 如果从 SPEAKING 状态切换出去,确保音频播放完成
if old_state == DeviceState.SPEAKING:
self.audio_codec.wait_for_audio_complete()
self.device_state = state
logger.info(f"状态变更: {old_state} -> {state}")
# 根据状态执行相应操作
VAD.pause() # 停用 VAD
self.audio_codec.stop_streams() # 停用输入输出流
if state == DeviceState.IDLE:
self.display.update_status("待命")
self.display.update_emotion("😶")
# 停止输出流但不关闭它
if (
self.audio_codec.output_stream
and self.audio_codec.output_stream.is_active()
):
try:
self.audio_codec.output_stream.stop_stream()
except Exception as e:
logger.warning(f"停止输出流时出错: {e}")
elif state == DeviceState.CONNECTING:
self.display.update_status("连接中...")
elif state == DeviceState.LISTENING:
self.display.update_status("聆听中...")
self.display.update_emotion("🙂")
if (
self.audio_codec.input_stream
and not self.audio_codec.input_stream.is_active()
):
try:
self.audio_codec.input_stream.start_stream()
except Exception as e:
logger.warning(f"启动输入流时出错: {e}")
# 使用 AudioCodec 类中的方法重新初始化
self.audio_codec._reinitialize_input_stream()
# 停止输出流
if self.audio_codec.output_stream.is_active():
self.audio_codec.output_stream.stop_stream()
# 打开输入流
if not self.audio_codec.input_stream.is_active():
self.audio_codec.input_stream.start_stream()
elif state == DeviceState.SPEAKING:
self.display.update_status("说话中...")
# 确保输出流处于活跃状态
if self.audio_codec.output_stream:
if not self.audio_codec.output_stream.is_active():
try:
self.audio_codec.output_stream.start_stream()
except Exception as e:
logger.warning(f"启动输出流时出错: {e}")
# 使用 AudioCodec 类中的方法重新初始化
self.audio_codec._reinitialize_output_stream()
# 停止输入流
if (
self.audio_codec.input_stream
and self.audio_codec.input_stream.is_active()
):
try:
self.audio_codec.input_stream.stop_stream()
except Exception as e:
logger.warning(f"停止输入流时出错: {e}")
if self.audio_codec.input_stream.is_active():
self.audio_codec.input_stream.stop_stream()
# 打开输出流
if not self.audio_codec.output_stream.is_active():
self.audio_codec.output_stream.start_stream()
# 通知状态变化
for callback in self.on_state_changed_callbacks:
try:
callback(state)
except Exception as e:
logger.error(f"执行状态变化回调时出错: {e}")
except Exception:
pass
def _get_status_text(self):
"""获取当前状态文本"""
@@ -602,105 +485,33 @@ class XiaoZhi:
def _start_listening_impl(self):
"""开始监听的实现"""
if not self.protocol:
logger.error("协议未初始化")
return
self.keep_listening = False
self.set_device_state(DeviceState.IDLE)
asyncio.run_coroutine_threadsafe(
self.protocol.send_abort_speaking(AbortReason.ABORT),
self.loop,
)
if self.device_state == DeviceState.IDLE:
# 尝试打开音频通道
if not self.protocol.is_audio_channel_opened():
self.set_device_state(DeviceState.CONNECTING) # 设置设备状态为连接中
try:
# 等待异步操作完成
future = asyncio.run_coroutine_threadsafe(
self.protocol.open_audio_channel(), self.loop
)
# 等待操作完成并获取结果
assert future.result(timeout=10.0) # 添加超时时间
except Exception as e:
self.alert("错误", f"打开音频通道失败: {str(e)}")
self.set_device_state(DeviceState.IDLE)
return
# 尝试打开音频通道
if not self.protocol.is_audio_channel_opened():
try:
# 等待异步操作完成
future = asyncio.run_coroutine_threadsafe(
self.protocol.open_audio_channel(), self.loop
)
# 等待操作完成并获取结果
success = future.result(timeout=10.0) # 添加超时时间
if not success:
self.alert("错误", "打开音频通道失败") # 弹出错误提示
self.set_device_state(DeviceState.IDLE) # 设置设备状态为空闲
return
except Exception as e:
logger.error(f"打开音频通道时发生错误: {e}")
self.alert("错误", f"打开音频通道失败: {str(e)}")
self.set_device_state(DeviceState.IDLE)
return
asyncio.run_coroutine_threadsafe(
self.protocol.send_start_listening(ListeningMode.MANUAL), self.loop
)
self.set_device_state(DeviceState.LISTENING) # 设置设备状态为监听中
elif self.device_state == DeviceState.SPEAKING:
if not self.aborted:
self.abort_speaking(AbortReason.WAKE_WORD_DETECTED)
async def _open_audio_channel_and_start_manual_listening(self):
"""打开音频通道并开始手动监听"""
if not await self.protocol.open_audio_channel():
self.set_device_state(DeviceState.IDLE)
self.alert("错误", "打开音频通道失败")
return
await self.protocol.send_start_listening(ListeningMode.MANUAL)
self.set_device_state(DeviceState.LISTENING)
def toggle_chat_state(self):
"""切换聊天状态"""
self.schedule(self._toggle_chat_state_impl)
def _toggle_chat_state_impl(self):
"""切换聊天状态的具体实现"""
# 检查协议是否已初始化
if not self.protocol:
logger.error("协议未初始化")
return
# 如果设备当前处于空闲状态,尝试连接并开始监听
if self.device_state == DeviceState.IDLE:
self.set_device_state(DeviceState.CONNECTING) # 设置设备状态为连接中
# 尝试打开音频通道
if not self.protocol.is_audio_channel_opened():
try:
# 等待异步操作完成
future = asyncio.run_coroutine_threadsafe(
self.protocol.open_audio_channel(), self.loop
)
# 等待操作完成并获取结果
success = future.result(timeout=10.0) # 添加超时时间
if not success:
self.alert("错误", "打开音频通道失败") # 弹出错误提示
self.set_device_state(DeviceState.IDLE) # 设置设备状态为空闲
return
except Exception as e:
logger.error(f"打开音频通道时发生错误: {e}")
self.alert("错误", f"打开音频通道失败: {str(e)}")
self.set_device_state(DeviceState.IDLE)
return
self.keep_listening = True # 开始监听
# 启动自动停止的监听模式
asyncio.run_coroutine_threadsafe(
self.protocol.send_start_listening(ListeningMode.AUTO_STOP), self.loop
)
self.set_device_state(DeviceState.LISTENING) # 设置设备状态为监听中
# 如果设备正在说话,停止当前说话
elif self.device_state == DeviceState.SPEAKING:
self.abort_speaking(AbortReason.NONE) # 中止说话
# 如果设备正在监听,关闭音频通道
elif self.device_state == DeviceState.LISTENING:
asyncio.run_coroutine_threadsafe(
self.protocol.close_audio_channel(), self.loop
)
asyncio.run_coroutine_threadsafe(
self.protocol.send_start_listening(ListeningMode.MANUAL), self.loop
)
self.set_device_state(DeviceState.LISTENING) # 设置设备状态为监听中
def stop_listening(self):
"""停止监听"""
@@ -708,35 +519,19 @@ class XiaoZhi:
def _stop_listening_impl(self):
"""停止监听的实现"""
if self.device_state == DeviceState.LISTENING:
asyncio.run_coroutine_threadsafe(
self.protocol.send_stop_listening(), self.loop
)
self.set_device_state(DeviceState.IDLE)
self.set_device_state(DeviceState.IDLE)
asyncio.run_coroutine_threadsafe(self.protocol.send_stop_listening(), self.loop)
def abort_speaking(self, reason):
"""中止语音输出"""
logger.info(f"中止语音输出,原因: {reason}")
self.aborted = True
asyncio.run_coroutine_threadsafe(
self.protocol.send_abort_speaking(reason), self.loop
)
self.set_device_state(DeviceState.IDLE)
# 添加此代码:当用户主动打断时自动进入录音模式
if reason == AbortReason.WAKE_WORD_DETECTED and self.keep_listening:
# 短暂延迟确保abort命令被处理
def start_listening_after_abort():
time.sleep(0.2) # 短暂延迟
self.set_device_state(DeviceState.IDLE)
self.schedule(lambda: self.toggle_chat_state())
threading.Thread(target=start_listening_after_abort, daemon=True).start()
asyncio.run_coroutine_threadsafe(
self.protocol.send_abort_speaking(AbortReason.ABORT),
self.loop,
)
def alert(self, title, message):
"""显示警告信息"""
logger.warning(f"警告: {title}, {message}")
# 在GUI上显示警告
if self.display:
self.display.update_text(f"{title}: {message}")
@@ -746,7 +541,6 @@ class XiaoZhi:
def shutdown(self):
"""关闭应用程序"""
logger.info("正在关闭应用程序...")
self.running = False
# 关闭音频编解码器
@@ -767,42 +561,10 @@ class XiaoZhi:
if self.loop_thread and self.loop_thread.is_alive():
self.loop_thread.join(timeout=1.0)
logger.info("应用程序已关闭")
def _handle_verification_code(self, text):
"""处理验证码信息"""
try:
# 提取验证码
import re
verification_code = re.search(r"验证码:(\d+)", text)
if verification_code:
code = verification_code.group(1)
# 尝试打开浏览器
try:
import webbrowser
if webbrowser.open("https://xiaozhi.me/login"):
logger.info("已打开登录页面")
else:
logger.warning("无法打开浏览器")
except Exception as e:
logger.warning(f"打开浏览器时出错: {e}")
# 无论如何都显示验证码
self.alert("验证码", f"您的验证码是: {code}")
except Exception as e:
logger.error(f"处理验证码时出错: {e}")
def toggle_chat_state(self):
"""切换聊天状态"""
pass
def _on_mode_changed(self, auto_mode):
"""处理对话模式变更"""
# 只有在IDLE状态下才允许切换模式
if self.device_state != DeviceState.IDLE:
self.alert("提示", "只有在待命状态下才能切换对话模式")
return False
self.keep_listening = auto_mode
logger.info(f"对话模式已切换为: {'自动' if auto_mode else '手动'}")
return True
pass
+37 -19
View File
@@ -1,7 +1,7 @@
# OpenXiaoAI Patch
> [!CAUTION]
> 刷机有风险,操作需谨慎。
> 刷机有风险,操作需谨慎。请勿下载使用不明来历的固件!
小爱音箱 Pro 补丁固件制作流程:
- 固件提取(登录小米账号获取 OTA 链接)
@@ -9,32 +9,37 @@
- 禁用系统自动更新(系统更新后需要重新刷机打补丁)
- 添加开机启动脚本 `/data/init.sh`(方便执行一些初始化脚本)
## 准备条件
> [!IMPORTANT]
> 本教程仅适用于 **小爱音箱 ProLX06** 和 **Xiaomi 智能音箱 ProOH2P** 这两款机型,**其他型号**的小爱音箱可能存在兼容性问题,请勿直接使用!🚨
- 小爱音箱 ProLX06、OH2P
- Windows 电脑(需为 x86_64 架构,用于刷机)
- 数据线(不能只是充电线,需要连接到电脑传输数据)
- Type-C(适用于新款小爱音箱 Pro,无需拆机)
- Micro USB(旧款小爱音箱 Pro 用这种,需要拆机)
## 下载固件
你可以直接在 [Github Releases](https://github.com/idootop/open-xiaoai/releases) 页面下载打包好的固件:
- [Xiaomi 智能音箱 Pro v1.56.19](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.56.19)
- [小爱音箱 Pro v1.88.206](https://github.com/idootop/open-xiaoai/releases/tag/LX06_1.88.206)
> [!TIP]
> 里面有两个文件,下载 `patched` 那个:
> - `xxx_patched.squashfs` 打补丁后的固件
> - `xxx.squashfs` 原版固件(可用来刷回原系统)
> [!NOTE]
> 构建版本待发布,敬请期待
> 默认 SSH 登录密码为 `open-xiaoai`,如需修改请自行制作固件
你可以直接在 Github Release 页面下载打包好的补丁固件。
> [!IMPORTANT]
> 请下载和你当前小爱音箱版本一致的固件,跨版本刷机可能会出现未知错误,导致设备变砖。
> 如果上面没有你的版本,请升级设备固件到最新版本,或者按照下面的教程自行制作固件。
> [!CAUTION]
> 当前支持的最新固件版本为:
> - Xiaomi 智能音箱 Pro 👉 [v1.56.19](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.56.19)
> - 小爱音箱 Pro 👉 [v1.88.206](https://github.com/idootop/open-xiaoai/releases/tag/LX06_1.88.206)
>
> 更新版本的固件可能存在变化,导致刷机失败,设备变砖,请自行评估风险。
> 默认 SSH 登录密码为 `open-xiaoai`
## 制作固件
可以按照下面的 2 种方法,制作自定义固件。
你可以按照下面的 2 种方法,制作自定义固件。
### 基础配置
@@ -61,7 +66,7 @@ SSH_PASSWORD=open-xiaoai
- Dockerhttps://www.docker.com/get-started/
> [!NOTE]
> Windows 系统推荐使用 Git Bash 终端运行。
> Windows 系统推荐使用 [Git Bash](https://git-scm.com/downloads) 终端运行。
>
> CMD 和 PowerShell 终端需要调整下面命令中文件(夹)的实际路径。
@@ -93,3 +98,16 @@ npm run build
> [!TIP]
> 如果你想要更进一步的定制自己的固件,可以参考 `src/build.sh` 脚本里的构建流程:在提取固件后自行修改固件内的脚本、配置和应用程序,然后重新打包即可。
## 高级选项
### 1. 自定义启动脚本
默认修改后的补丁固件,会将 `/data/init.sh` 文件作为启动脚本,开机时自动运行。如果你需要自定义开机启动脚本,可自行创建和修改该文件。
示例:
```bash
#! /bin/sh
/usr/sbin/tts_play.sh '初始化成功'
```
+1 -1
View File
@@ -4,6 +4,6 @@
# Put your custom commands here that should be executed once
# the system init finished. By default this file does nothing.
+[ -f "/data/init.sh" ] && /data/init.sh &
+[ -f "/data/init.sh" ] && sh /data/init.sh &
+
exit 0
@@ -1,6 +1,24 @@
--- squashfs-root/etc/asound.conf 2024-12-06 15:11:58
+++ squashfs-root-patched/etc/asound.conf 2025-02-18 22:14:10
@@ -125,16 +125,20 @@
@@ -109,32 +109,32 @@
type plug
slave.pcm noop
}
+
pcm.mico_record {
type plug
- slave.pcm noop
- ttable {
- 0.4 1
- 1.2 1
- 2.0 1
- 3.6 1
- 4.6 1
- 5.6 1
- 6.6 1
- 7.6 1
- }
+ slave.pcm Capture
}
pcm.noop {
@@ -14,20 +32,25 @@
- period_size 480
- buffer_size 3840
- }
+ type plug
+ slave.pcm {
+ type plug
+ slave.pcm Capture
}
+pcm.Capture {
+ type plug
+ slave.pcm {
+ type dsnoop
+ ipc_key 1024
+ ipc_perm 0666
+ slave {
+ pcm "hw:0,3"
+ format S32_LE
+ rate 48000
+ format S32_LE
+ channels 8
+ period_size 480
+ buffer_size 3840
+ period_size 384
+ buffer_size 6144
+ }
+ }
}
+}
+
defaults.pcm.rate_converter "speexrate_medium"
@@ -0,0 +1,57 @@
--- squashfs-root/etc/asound.conf.dts 2024-12-06 15:11:58
+++ squashfs-root-patched/etc/asound.conf.dts 2025-02-18 22:14:10
@@ -109,32 +109,32 @@
type plug
slave.pcm noop
}
+
pcm.mico_record {
- type plug
- slave.pcm noop
- ttable {
- 0.4 1
- 1.2 1
- 2.0 1
- 3.6 1
- 4.6 1
- 5.6 1
- 6.6 1
- 7.6 1
- }
+ type plug
+ slave.pcm Capture
}
pcm.noop {
- type dsnoop
- ipc_key 1024
- slave {
- pcm "hw:0,3"
- format S32_LE
- rate 48000
- channels 8
- period_size 480
- buffer_size 3840
- }
+ type plug
+ slave.pcm Capture
}
+pcm.Capture {
+ type plug
+ slave.pcm {
+ type dsnoop
+ ipc_key 1024
+ ipc_perm 0666
+ slave {
+ pcm "hw:0,3"
+ rate 48000
+ format S32_LE
+ channels 8
+ period_size 384
+ buffer_size 6144
+ }
+ }
+}
+
defaults.pcm.rate_converter "speexrate_medium"
+1 -1
View File
@@ -106,7 +106,7 @@ async function main() {
if (filePath) {
fs.writeFileSync(
path.join(process.cwd(), "assets", ".model"),
ota.model
ota.model.toUpperCase()
);
fs.writeFileSync(
path.join(process.cwd(), "assets", ".version"),
+3 -3
View File
@@ -308,6 +308,7 @@ name = "open-xiaoai"
version = "1.0.0"
dependencies = [
"futures",
"rand",
"serde",
"serde_json",
"tokio",
@@ -385,13 +386,12 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
"zerocopy",
]
[[package]]
+6 -5
View File
@@ -4,11 +4,11 @@ version = "1.0.0"
edition = "2021"
[profile.release]
lto = true
opt-level = "s"
codegen-units = 1
panic = "abort"
strip = true
lto = true
opt-level = "s"
codegen-units = 1
panic = "abort"
strip = true
debug = false
[workspace.metadata.cross.target.armv7-unknown-linux-gnueabihf]
@@ -21,3 +21,4 @@ tokio-tungstenite = "0.26.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
uuid = { version = "1.15.1", features = ["v4"] }
rand = "0.9.1"
+49 -22
View File
@@ -7,28 +7,53 @@
- 转发小爱音箱上的事件到 Server 端(语音识别结果、播放状态等)
- 响应来自 Server 端的指令调用(执行脚本、播放音频流、系统升级等)
> [!TIP]
> 本项目 Rust 端通过 binding 与 Python 和 Node.js 端双向互调,实现了网络通信模块的共享复用。当然你也可以参考 Rust 端通信协议源码,在其他 Server 端重新实现 WebSocket 通信过程。
## 快速开始
> [!NOTE]
> 以下操作需要先将小爱音箱刷机, 然后 SSH 连接到小爱音箱。👉 [教程](../../docs/flash.md)
```shell
# 创建 open-xiaoai 文件夹
mkdir /data/open-xiaoai
# 设置 server 地址(注意替换成自己的 server 地址)
echo 'ws://192.168.31.227:4399' > /data/open-xiaoai/server.txt
# 运行启动脚本 init.sh
curl -sSfL https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-client/init.sh | sh
```
> [!IMPORTANT]
> 你可能需要先在电脑上运行其他演示程序,以获取 server 地址
>
> 注意安全!不要连接来路不明的 server 🚨
如果你想要开机自启动,运行以下命令重启小爱音箱即可。
```shell
# 下载 boot.sh 文件到 /data/init.sh 开机时自启动
curl -L -o /data/init.sh https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-client/boot.sh
# 重启小爱音箱
reboot
```
## 编译运行
> [!IMPORTANT]
> 本项目仅作为挖掘小爱音箱潜力的一个演示,并未提供任何预构建产物,仅适合有动手能力的人自行编译运行。你可以按需修改源代码,增删自己想要的功能。
> [!TIP]
> 如果你是一名开发者,想要修改源代码实现自己想要的功能,可以按照下面的步骤,自行编译运行该项目
首先,你需要在电脑上安装 `Rust` 开发环境 👉 [传送门](https://www.rust-lang.org/)
为了构建能够在小爱音箱上运行的 ARMv7 应用,你还需要安装 `cross` 👉 [传送门](https://github.com/cross-rs/cross)
```shell
# 安装依赖
cargo fetch
# 交叉编译
cross build --release --target armv7-unknown-linux-gnueabihf
```
> [!NOTE]
> 以下操作需要先将小爱音箱刷机, 然后 SSH 连接到小爱音箱。
> [!TIP]
> 如果你是 Apple silicon 芯片,为了能够正常使用 cross 交叉编译镜像,请先在 Docker Desktop - Settings - General - Virtual Machine Options 中打开 Apple Virtual framework 选项,然后开启 `Use Rosetta for x86_64/amd64 emulation on Apple Silicon`
编译成功后,将构建好的补丁程序 `client` 复制到小爱音箱上
@@ -41,10 +66,13 @@ cross build --release --target armv7-unknown-linux-gnueabihf
```shell
dd if=target/armv7-unknown-linux-gnueabihf/release/client \
| ssh -o HostKeyAlgorithms=+ssh-rsa root@你的小爱音箱IP地址 "dd of=/data/client"
| ssh -o HostKeyAlgorithms=+ssh-rsa root@你的小爱音箱IP地址 "dd of=/data/open-xiaoai/client"
```
> 注意:替换你自己的小爱音箱局域网 IP 地址,比如: root@192.168.31.227
> [!TIP]
> 注意替换你自己的小爱音箱局域网 IP 地址,比如: root@192.168.31.227
>
> 如果提示 No such file or directory 请先在小爱音箱上创建 `/data/open-xiaoai` 文件夹
你也可以先把 `client` 文件上传到一个地方,然后 SSH 连接到小爱音箱后,再用 `curl` 命令下载到本地。
@@ -52,29 +80,27 @@ dd if=target/armv7-unknown-linux-gnueabihf/release/client \
# 连接到小爱音箱
ssh -o HostKeyAlgorithms=+ssh-rsa root@你的小爱音箱IP地址
# 下载文件到 /data/client
curl -# -o /data/client https://你的client文件下载链接
# 创建 open-xiaoai 文件夹
mkdir /data/open-xiaoai
# 下载文件
curl -# -o /data/open-xiaoai/client https://你的client文件下载链接
```
最后,在小爱音箱上授予 `client` 文件运行权限,然后运行:
```shell
# 授权
chmod +x /data/client
chmod +x /data/open-xiaoai/client
# 运行
/data/client ws://你的 server 端地址
/data/open-xiaoai/client ws://你的 server 端地址(默认使用 4399 端口)
# 比如:/data/client ws://192.168.31.227:4399
# 比如:/data/open-xiaoai/client ws://192.168.31.227:4399
```
## 注意事项
> [!TIP]
> 旧版小爱音箱 Pro 的可用 RAM 和 ROM 均为 10MB 左右
>
> 新版 Xiaomi 智能音箱 Pro 约为 100MB(升级为旧版的 10 倍!)
当前 Client 端使用 Rust 编写,只负责转发和被动响应 Server 端的调用,不实现具体的业务逻辑。
一方面,小爱音箱的内存算力和存储空间极为有限,语音识别之类的任务并不适合放在 Client 端侧运行。
@@ -88,6 +114,7 @@ chmod +x /data/client
其默认提供了**执行任意脚本**的能力演示,虽然在启动 Client 端时需要由你本人指定可信的 Server 端连接地址,但还是要务必小心!
毕竟再怎么小心都不为过 :)
> [!TIP]
> 本项目 Rust 端通过 binding 与 Python 和 Node.js 端双向互调,实现了网络通信模块的共享复用。当然你也可以参考 Rust 端通信协议源码,在其他 Server 端重新实现 WebSocket 通信过程。
你也可以 Fork 本项目,在此基础上将 MiGPT 和小智 AI 等业务逻辑完全放在 Client 端运行,这样就不需要额外的服务器或 NAS 设备来部署运行了。
+33
View File
@@ -0,0 +1,33 @@
#! /bin/sh
set -e
DOWNLOAD_BASE_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-client"
WORK_DIR="/data/open-xiaoai"
CLIENT_BIN="$WORK_DIR/client"
SERVER_ADDRESS="ws://127.0.0.1:4399" # 默认不会连接到任何 server
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$CLIENT_BIN" ]; then
echo "🔥 正在下载 Client 端补丁程序..."
curl -L -# -o "$CLIENT_BIN" "$DOWNLOAD_BASE_URL/client"
chmod +x "$CLIENT_BIN"
echo "✅ Client 端补丁程序下载完毕"
fi
if [ -f "$WORK_DIR/server.txt" ]; then
SERVER_ADDRESS=$(cat "$WORK_DIR/server.txt")
fi
echo "🔥 正在启动 Client 端补丁程序..."
kill -9 `ps|grep "open-xiaoai/client"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
"$CLIENT_BIN" "$SERVER_ADDRESS" > /dev/null 2>&1
+33
View File
@@ -0,0 +1,33 @@
#! /bin/sh
set -e
DOWNLOAD_BASE_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-client"
WORK_DIR="/data/open-xiaoai"
CLIENT_BIN="$WORK_DIR/client"
SERVER_ADDRESS="ws://127.0.0.1:4399" # 默认不会连接到任何 server
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$CLIENT_BIN" ]; then
echo "🔥 正在下载 Client 端补丁程序..."
curl -L -# -o "$CLIENT_BIN" "$DOWNLOAD_BASE_URL/client"
chmod +x "$CLIENT_BIN"
echo "✅ Client 端补丁程序下载完毕"
fi
if [ -f "$WORK_DIR/server.txt" ]; then
SERVER_ADDRESS=$(cat "$WORK_DIR/server.txt")
fi
echo "🔥 正在启动 Client 端补丁程序..."
kill -9 `ps|grep "open-xiaoai/client"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
"$CLIENT_BIN" "$SERVER_ADDRESS"

Some files were not shown because too many files have changed in this diff Show More