Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1405eca23f | ||
|
|
43e6739726 | ||
|
|
affd94d1cf | ||
|
|
53f8ab31ee | ||
|
|
c5edea969c | ||
|
|
e4dc39ef39 | ||
|
|
729ba86d43 | ||
|
|
0075f13ffd | ||
|
|
96d69ec8a6 | ||
|
|
c0b11436fd | ||
|
|
948364dcc9 | ||
|
|
c06bbf6c51 | ||
|
|
f2ba7b496d | ||
|
|
aa5d4490bb | ||
|
|
e3609fc888 | ||
|
|
ba828b782d | ||
|
|
bcf98c5365 | ||
|
|
8e7165e636 | ||
|
|
d83c2b3093 | ||
|
|
824ff6bc68 | ||
|
|
db642fc176 | ||
|
|
7974ab3f2b | ||
|
|
c34c884f86 | ||
|
|
c61f44e5ed | ||
|
|
b2eba4bb89 | ||
|
|
66d222bdf7 | ||
|
|
58acbc29f4 | ||
|
|
59c3280938 | ||
|
|
e687280ab1 | ||
|
|
6bdd1b9683 | ||
|
|
c8e7861bdb | ||
|
|
8fc5f073b5 | ||
|
|
6eca122f24 | ||
|
|
9a8e07a68e | ||
|
|
cde047af19 | ||
|
|
a73ef08074 | ||
|
|
cf853bb07b | ||
|
|
9129afcc99 | ||
|
|
a2c9cd5b70 | ||
|
|
b4f52ee5b9 | ||
|
|
73bf1c6f4d | ||
|
|
e39675abb8 | ||
|
|
95caae8aa4 | ||
|
|
8d5faa28dc | ||
|
|
f8d4599cc4 | ||
|
|
e9e4ca92dd | ||
|
|
3f85e07ba7 | ||
|
|
ca728e3dfc | ||
|
|
fc9409e55f | ||
|
|
3aa0cf673b | ||
|
|
e3732a7f1c | ||
|
|
62c7c88eab | ||
|
|
20c21ca609 | ||
|
|
a74d92b4b6 | ||
|
|
4a0fde4724 | ||
|
|
755ba2f202 | ||
|
|
767b7c4548 | ||
|
|
2cb5300577 | ||
|
|
2ebfb2241a | ||
|
|
c3cac9b2b4 | ||
|
|
39febdfe57 | ||
|
|
012a123fdc | ||
|
|
e53bc65715 | ||
|
|
0900b4cf2a | ||
|
|
4e7b6f6026 | ||
|
|
10c464a4e9 | ||
|
|
487848d9b0 | ||
|
|
66dc6ef741 | ||
|
|
b63a8f95a6 | ||
|
|
9ab2ec4058 | ||
|
|
1bdea51c3a | ||
|
|
9506c83223 | ||
|
|
29f4f77c7a | ||
|
|
b7716e8654 | ||
|
|
8838087fd7 | ||
|
|
6d04185792 | ||
|
|
e0f4dba612 | ||
|
|
3280809844 | ||
|
|
80b5ae4db7 | ||
|
|
8f85d2dd56 | ||
|
|
89019c7125 | ||
|
|
a863da3067 | ||
|
|
aebf94d5cd | ||
|
|
949dee9500 | ||
|
|
05072facfd |
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -2,6 +2,8 @@
|
||||
|
||||
让小爱音箱「听见你的声音」,解锁无限可能。
|
||||
|
||||

|
||||
|
||||
## 简介
|
||||
|
||||
2017 年,当全球首款千万级销量的智能音箱诞生时,我们以为触摸到了未来。但很快发现,这些设备被困在「指令-响应」的牢笼里:
|
||||
@@ -10,7 +12,7 @@
|
||||
- 它能执行命令,却不会主动思考
|
||||
- 它有千万用户,却只有一套思维
|
||||
|
||||
我们曾幻想中的"贾维斯"级人工智能,在现实场景中沦为"闹钟+音乐播放器"。
|
||||
我们曾幻想中的"贾维斯"级人工智能,在现实场景中沦为"天气预报+音乐播放器"。
|
||||
|
||||
**真正的智能不应被预设的代码逻辑所束缚,而应像生命体般在交互中进化。**
|
||||
|
||||
@@ -24,43 +26,64 @@
|
||||
|
||||
## 你的声音 + 小爱音箱 = 无限可能
|
||||
|
||||
👉 [小爱音箱接入小智 AI 演示视频](https://www.bilibili.com/video/BV1NBXWYSEvX)
|
||||
👉 [小爱音箱接入小智 AI 演示视频](https://www.bilibili.com/video/BV1TxJhzvEhz)
|
||||
|
||||
[](https://www.bilibili.com/video/BV1NBXWYSEvX)
|
||||
[](https://www.bilibili.com/video/BV1TxJhzvEhz)
|
||||
|
||||
👉 [小爱音箱自定义唤醒词演示视频](https://www.bilibili.com/video/BV1YfVUz5EMj)
|
||||
|
||||
[](https://www.bilibili.com/video/BV1YfVUz5EMj)
|
||||
|
||||
👉 [小爱音箱接入 MiGPT 演示视频](https://www.bilibili.com/video/BV1N1421y7qn)
|
||||
|
||||
[](https://www.bilibili.com/video/BV1N1421y7qn)
|
||||
[](https://www.bilibili.com/video/BV1N1421y7qn)
|
||||
|
||||
## 刷机教程
|
||||
## 快速开始
|
||||
|
||||
> [!CAUTION]
|
||||
> 刷机有风险,操作需谨慎。
|
||||
|
||||
整理中,敬请期待 :)
|
||||
|
||||
## 使用说明
|
||||
> [!IMPORTANT]
|
||||
> 本教程仅适用于 **小爱音箱 Pro(LX06)** 和 **Xiaomi 智能音箱 Pro(OH2P)** 这两款机型,**其他型号**的小爱音箱请勿直接使用!🚨
|
||||
|
||||
本项目由 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)
|
||||
|
||||
以上皆为抛砖引玉,你也可以自己亲手编写想要的功能,一切由你定义!
|
||||
以上皆为抛砖引玉,你也可以亲手编写自己想要的功能,一切由你定义!
|
||||
|
||||
## 相关项目
|
||||
|
||||
> [!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),否则请立即终止使用并彻底删除本项目。
|
||||
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
## 授权范围
|
||||
|
||||
本项目授权范围仅限于技术原理研究、安全漏洞验证及非营利性个人使用。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
|
||||
本项目仅供学术研究或个人测试用途。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
|
||||
|
||||
## 非官方声明
|
||||
|
||||
本项目为非盈利开源项目,由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务等知识产权均归属权利方所有。若权利方主张权益,用户应立即终止使用并彻底销毁项目副本。
|
||||
本项目为开源非营利项目,由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,亦未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务等知识产权均归属权利方所有。若权利方主张权益,用户应立即终止使用并彻底销毁项目副本。
|
||||
|
||||
## 风险告知
|
||||
|
||||
|
||||
@@ -0,0 +1,115 @@
|
||||
# 小爱音箱 Pro 刷机教程
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 本教程仅适用于 **小爱音箱 Pro(LX06)** 和 **Xiaomi 智能音箱 Pro(OH2P)** 这两款机型,**其他型号**的小爱音箱请勿直接使用!🚨
|
||||
|
||||
### 准备条件
|
||||
|
||||
- 小爱音箱
|
||||
- Xiaomi 智能音箱 Pro(OH2P)
|
||||
- 小爱音箱 Pro(LX06)
|
||||
- 数据线
|
||||
- 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 数据线连接到电脑。
|
||||
|
||||

|
||||
|
||||
### 开始刷机
|
||||
|
||||
> [!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)。
|
||||
|
||||

|
||||
|
||||
然后运行 `AMLLogic driver installer.exe` 文件,按照提示安装 USB 驱动。
|
||||
|
||||

|
||||
|
||||
安装好驱动后,打开 `/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"
|
||||
```
|
||||
|
After Width: | Height: | Size: 352 KiB |
|
After Width: | Height: | Size: 525 KiB |
|
After Width: | Height: | Size: 342 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
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"] }
|
||||
@@ -0,0 +1,39 @@
|
||||
# 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` 文件中的密钥。
|
||||
|
||||
## 环境准备
|
||||
|
||||
为了能够正常编译运行该项目,你需要安装以下依赖环境/工具:
|
||||
|
||||
- uv:https://github.com/astral-sh/uv
|
||||
- Rust: https://www.rust-lang.org/learn/get-started
|
||||
|
||||
## 编译运行
|
||||
|
||||
> [!NOTE]
|
||||
> 请先确认你已经将小爱音箱刷机成功,并安装运行了 Rust 补丁程序 [👉 教程](../../packages/client-rust/README.md),否则该项目启动后收不到音频输入,将无法正常工作。
|
||||
|
||||
```bash
|
||||
# 克隆代码
|
||||
git clone https://github.com/idootop/open-xiaoai.git
|
||||
|
||||
# 进入当前项目根目录
|
||||
cd examples/gemini
|
||||
|
||||
# 安装 Python 依赖
|
||||
uv sync --locked
|
||||
|
||||
# 编译运行
|
||||
uv run main.py
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
该演示暂不支持中断 AI 的回复,需要等待 AI 回答完毕后才能重新响应用户的语音输入。
|
||||
|
||||
> [!NOTE]
|
||||
> 本项目只是一个简单的演示程序,抛砖引玉。如果你想要更多的功能,可以借助 Python 丰富的 AI 生态自行实现。
|
||||
@@ -0,0 +1,4 @@
|
||||
from .gemini import Gemini
|
||||
from .xiaoai import XiaoAi
|
||||
|
||||
__all__ = ["Gemini", "XiaoAi"]
|
||||
@@ -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)
|
||||
@@ -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)
|
||||
@@ -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())
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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 },
|
||||
]
|
||||
@@ -0,0 +1,157 @@
|
||||
# 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) 项目。
|
||||
|
||||
### Q:我是小爱音箱 Pro(LX06)为什么启动失败显示 Killed?
|
||||
|
||||
目前只在新款小米智能音箱 Pro(OH2P)和旧款带 DTS 功能的小爱音箱 Pro(256 MB 内存)上测试可以正常运行。
|
||||
|
||||
有些新版的小爱音箱 Pro(不带 DTS 版本)内存仅为 128 MB,无法提供充足的内存来加载模型文件,故无法在 Client 端运行该 KWS 服务。
|
||||
|
||||
建议参考 [xiaozhi](../xiaozhi/README.md) 演示,在 Server 端运行 KWS 服务。
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/bin/sh
|
||||
|
||||
exec > /dev/null 2>&1
|
||||
|
||||
cat << 'EOF'
|
||||
|
||||
▄▖ ▖▖▘ ▄▖▄▖
|
||||
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
|
||||
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
|
||||
▌
|
||||
|
||||
v1.0.0 by: https://del.wang
|
||||
|
||||
EOF
|
||||
|
||||
set -e
|
||||
|
||||
echo "🤫 等待网络连接中..."
|
||||
|
||||
sleep 5
|
||||
|
||||
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
|
||||
@@ -0,0 +1,75 @@
|
||||
#!/bin/sh
|
||||
|
||||
cat << 'EOF'
|
||||
|
||||
▄▖ ▖▖▘ ▄▖▄▖
|
||||
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
|
||||
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
|
||||
▌
|
||||
|
||||
v1.0.0 by: https://del.wang
|
||||
|
||||
EOF
|
||||
|
||||
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
|
||||
@@ -0,0 +1,96 @@
|
||||
#!/bin/sh
|
||||
|
||||
cat << 'EOF'
|
||||
|
||||
▄▖ ▖▖▘ ▄▖▄▖
|
||||
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
|
||||
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
|
||||
▌
|
||||
|
||||
v1.0.0 by: https://del.wang
|
||||
|
||||
EOF
|
||||
|
||||
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
|
||||
@@ -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()
|
||||
@@ -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 ì @你好小智
|
||||
@@ -0,0 +1,4 @@
|
||||
天猫精灵
|
||||
小度小度
|
||||
豆包豆包
|
||||
你好小智
|
||||
@@ -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"] }
|
||||
@@ -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"]
|
||||
@@ -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 运行
|
||||
|
||||
[](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)),可参考本项目代码自行实现。
|
||||
@@ -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 };
|
||||
}
|
||||
},
|
||||
};
|
||||
@@ -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"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
3.12
|
||||
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
]
|
||||
@@ -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"
|
||||
@@ -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"]
|
||||
@@ -0,0 +1,169 @@
|
||||
# Open-XiaoAI x 小智 AI
|
||||
|
||||
[Open-XiaoAI](https://github.com/idootop/open-xiaoai) 的 Python 版 Server 端,用来演示小爱音箱接入[小智 AI](https://github.com/78/xiaozhi-esp32)。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 本项目只是一个简单的演示程序,抛砖引玉。诸如一些音频压缩、加密传输、多账号管理等功能并未提供,建议只在局域网内测试运行,不推荐部署在公网服务器上(消耗流量 100kb/s),请自行评估相关风险,合理使用。
|
||||
|
||||
- 小爱音箱接入小智 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 运行
|
||||
|
||||
[](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
|
||||
```
|
||||
|
||||
### 编译运行
|
||||
|
||||
为了能够正常编译运行该项目,你需要安装以下依赖环境/工具:
|
||||
|
||||
- uv:https://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
|
||||
|
||||
# 编译运行(GUI 模式,不支持唤醒词唤醒)
|
||||
uv run main.py
|
||||
|
||||
# 或者设置环境变量 CLI=true,开启 CLI 模式(支持自定义唤醒词)
|
||||
CLI=true uv run main.py
|
||||
```
|
||||
|
||||
如果你只是想体验一下小智 AI,请使用以下命令启动:
|
||||
|
||||
```bash
|
||||
uv run main.py --mode xiaozhi
|
||||
```
|
||||
|
||||
该模式下使用电脑的麦克风和扬声器作为音频输入输出设备,无需连接小爱音箱。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 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 服务器端的语音识别方案,与本项目无关。
|
||||
|
||||
### Q:唤醒词一直没有反应?
|
||||
|
||||
由于小爱音箱远场拾音音量较小,有时可能会识别不清,你可以调大 `config.py` 配置文件里的 `boost` 参数,然后重启应用 / Docker 试试看。
|
||||
|
||||
```py
|
||||
APP_CONFIG = {
|
||||
"vad": {
|
||||
# 小爱音箱录音音量较小,需要后期放大一下
|
||||
"boost": 100,
|
||||
# boost 调大后,语音检测阈值可能也需要一起调大些
|
||||
"threshold": 0.50,
|
||||
},
|
||||
# ... 其他配置
|
||||
}
|
||||
```
|
||||
|
||||
另外,应用 / Docker 刚刚启动时需要加载模型文件,比较耗时一些,可以等 30s 之后再试试看。
|
||||
|
||||
如果是英文唤醒词,可以尝试将最小发音用空格分开,比如:比如:'openai' 👉 'open ai'
|
||||
|
||||
PS:如果还是不行,建议更换其他更易识别的唤醒词。
|
||||
|
||||
### Q: 我想自己编译运行,模型文件在哪里下载?
|
||||
|
||||
由于 ASR 相关模型文件体积较大,并未直接提交在 git 仓库中,你可以在 release 中下载 [VAD + KWS 相关模型](https://github.com/idootop/open-xiaoai/releases/tag/vad-kws-models),然后解压到 `xiaozhi/models` 路径下即可。
|
||||
|
||||
## 相关项目
|
||||
|
||||
- [oxa-server](https://github.com/pu-007/oxa-server): 提供了更强大易用的 config.py 的配置方式
|
||||
|
||||
## 鸣谢
|
||||
|
||||
该演示使用的 Python 版小智 AI 客户端基于 [py-xiaozhi](https://github.com/Huang-junsen/py-xiaozhi) 项目,特此鸣谢。
|
||||
@@ -0,0 +1,72 @@
|
||||
import asyncio
|
||||
|
||||
|
||||
async def before_wakeup(speaker, text, source):
|
||||
"""
|
||||
处理收到的用户消息,并决定是否唤醒小智 AI
|
||||
|
||||
- source: 唤醒来源
|
||||
- 'kws': 关键字唤醒
|
||||
- 'xiaoai': 小爱同学收到用户指令
|
||||
"""
|
||||
if source == "kws":
|
||||
# 播放唤醒提示语
|
||||
await speaker.play(text="你好主人,我是小智,请问有什么吩咐?")
|
||||
# 返回 True 唤醒小智 AI
|
||||
return True
|
||||
|
||||
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",
|
||||
]
|
||||
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
/// 提供一个简便的日志记录宏,将消息传递给 Python 端
|
||||
///
|
||||
/// # 示例
|
||||
///
|
||||
/// ```
|
||||
/// pylog!("这是一条日志消息");
|
||||
/// pylog!("带有变量的日志: {}", variable);
|
||||
/// ```
|
||||
#[macro_export]
|
||||
macro_rules! pylog {
|
||||
($($arg:tt)*) => {
|
||||
crate::python::PythonManager::instance().log(format!($($arg)*));
|
||||
};
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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(())
|
||||
}
|
||||
@@ -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"
|
||||
@@ -0,0 +1,172 @@
|
||||
import asyncio
|
||||
|
||||
from config import APP_CONFIG
|
||||
from xiaozhi.ref import (
|
||||
get_audio_codec,
|
||||
get_kws,
|
||||
get_speaker,
|
||||
get_vad,
|
||||
get_xiaoai,
|
||||
get_xiaozhi,
|
||||
set_speech_frames,
|
||||
)
|
||||
from xiaozhi.services.protocols.typing import AbortReason, DeviceState, ListeningMode
|
||||
from xiaozhi.utils.base import get_env
|
||||
|
||||
|
||||
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 not get_env("CLI"):
|
||||
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):
|
||||
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, session_id):
|
||||
"""TTS结束"""
|
||||
if self.current_step in [Step.on_interrupt, Step.on_tts_end]:
|
||||
# 当前 session 已经被打断了,不再处理
|
||||
return
|
||||
self.session_id = self.session_id + 1
|
||||
self.update_step(Step.on_tts_end)
|
||||
self.start_session()
|
||||
|
||||
def on_tts_start(self, session_id):
|
||||
"""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 not get_env("CLI"):
|
||||
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
|
||||
|
||||
# 等待 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
|
||||
|
||||
# 开始说话
|
||||
set_speech_frames(speech_buffer)
|
||||
codec.input_stream.start_stream() # 开启录音
|
||||
await xiaozhi.protocol.send_start_listening(ListeningMode.MANUAL)
|
||||
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)
|
||||
|
||||
async def wakeup(self, text, source):
|
||||
before_wakeup = APP_CONFIG["wakeup"]["before_wakeup"]
|
||||
get_kws().pause() # 暂停 KWS 检测
|
||||
wakeup = await before_wakeup(get_speaker(), text, source)
|
||||
get_kws().resume() # 恢复 KWS 检测
|
||||
if wakeup:
|
||||
self.on_wakeup()
|
||||
|
||||
|
||||
EventManager = __EventManager()
|
||||
@@ -0,0 +1,7 @@
|
||||
天 猫 精 灵 @天猫精灵
|
||||
小 度 小 度 @小度小度
|
||||
豆 包 豆 包 @豆包豆包
|
||||
你 好 小 智 @你好小智
|
||||
你 好 小 爱 @你好小爱
|
||||
▁HI ▁S I RI
|
||||
▁HE Y ▁S I RI
|
||||
@@ -0,0 +1,59 @@
|
||||
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")
|
||||
|
||||
|
||||
def set_speech_frames(speech_frames: Any):
|
||||
GLOBAL_STATES["speech_frames"] = speech_frames
|
||||
|
||||
|
||||
def get_speech_frames() -> Any:
|
||||
return GLOBAL_STATES.get("speech_frames")
|
||||
@@ -0,0 +1,182 @@
|
||||
import opuslib_next as opuslib
|
||||
|
||||
from xiaozhi.ref import (
|
||||
get_speech_frames,
|
||||
get_xiaozhi,
|
||||
set_audio_codec,
|
||||
set_speech_frames,
|
||||
)
|
||||
from xiaozhi.services.audio.stream import MyAudio
|
||||
from xiaozhi.services.protocols.typing import AudioConfig
|
||||
from xiaozhi.utils.base import get_env
|
||||
|
||||
|
||||
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(
|
||||
output=True,
|
||||
format=AudioConfig.FORMAT,
|
||||
channels=AudioConfig.CHANNELS,
|
||||
rate=get_xiaozhi().protocol.server_sample_rate,
|
||||
frames_per_buffer=get_xiaozhi().protocol.server_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=get_xiaozhi().protocol.server_sample_rate,
|
||||
channels=AudioConfig.CHANNELS,
|
||||
)
|
||||
self.temp_frames = bytes([])
|
||||
|
||||
def read_audio(self):
|
||||
"""读取音频输入数据并编码"""
|
||||
try:
|
||||
speech_frames = get_speech_frames()
|
||||
|
||||
# 加入语音片段
|
||||
if speech_frames:
|
||||
self.temp_frames = speech_frames
|
||||
set_speech_frames([])
|
||||
|
||||
# 读取音频输入数据
|
||||
data = self.input_stream.read(
|
||||
num_frames=None if get_env("CLI") else AudioConfig.FRAME_SIZE,
|
||||
exception_on_overflow=False,
|
||||
)
|
||||
if not data:
|
||||
return None
|
||||
|
||||
self.temp_frames += data
|
||||
if len(self.temp_frames) < AudioConfig.FRAME_SIZE * 2:
|
||||
return None
|
||||
|
||||
opus_frames, remain_frames = self.encode_audio(self.temp_frames)
|
||||
self.temp_frames = remain_frames
|
||||
return opus_frames
|
||||
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):
|
||||
"""解码音频数据"""
|
||||
return self.opus_decoder.decode(
|
||||
opus_data,
|
||||
frame_size=get_xiaozhi().protocol.server_frame_size,
|
||||
decode_fec=False,
|
||||
)
|
||||
|
||||
def encode_audio(self, buffer: bytes, frame_size=AudioConfig.FRAME_SIZE):
|
||||
"""编码音频数据"""
|
||||
opus_frames = []
|
||||
remain_frames = bytes([])
|
||||
try:
|
||||
for i in range(0, len(buffer), frame_size * 2):
|
||||
chunk = buffer[i : i + frame_size * 2]
|
||||
if len(chunk) < frame_size * 2:
|
||||
remain_frames = chunk
|
||||
break
|
||||
opus_frame = self.opus_encoder.encode(chunk, frame_size)
|
||||
opus_frames.append(opus_frame)
|
||||
return opus_frames, remain_frames
|
||||
except Exception:
|
||||
return None, remain_frames
|
||||
|
||||
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,80 @@
|
||||
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.paused = False
|
||||
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 pause(self):
|
||||
self.paused = True
|
||||
|
||||
def resume(self):
|
||||
self.paused = False
|
||||
|
||||
def _detection_loop(self):
|
||||
SherpaOnnx.start()
|
||||
self.stream.start_stream()
|
||||
while True:
|
||||
# 读取缓冲区音频数据
|
||||
frames = self.stream.read()
|
||||
|
||||
# 在说话和监听状态时,暂停 KWS
|
||||
if (
|
||||
not frames
|
||||
or self.paused
|
||||
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(
|
||||
EventManager.wakeup(text, "kws"),
|
||||
get_xiaoai().async_loop,
|
||||
)
|
||||
|
||||
|
||||
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.clear()
|
||||
|
||||
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.clear()
|
||||
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,181 @@
|
||||
import threading
|
||||
import time
|
||||
|
||||
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 Silero
|
||||
from xiaozhi.services.protocols.typing import AudioConfig
|
||||
from xiaozhi.utils.base import get_env
|
||||
|
||||
|
||||
class _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
|
||||
|
||||
self.audio = None
|
||||
self.stream = None
|
||||
|
||||
# 暂存的语音片段
|
||||
self.silence_frames = [] # 静音片段
|
||||
self.speech_frames = [] # 语音片段
|
||||
self.target = None # 检测目标 speech/silence
|
||||
|
||||
def _reset_state(self):
|
||||
"""重置状态"""
|
||||
self.speech_count = 0
|
||||
self.silence_count = 0
|
||||
self.speech_frames = []
|
||||
self.silence_frames = []
|
||||
|
||||
def start(self):
|
||||
"""启动VAD检测器"""
|
||||
if not get_env("CLI"):
|
||||
return
|
||||
|
||||
self._initialize_audio_stream()
|
||||
|
||||
# 启动检测线程
|
||||
self.paused = False
|
||||
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
|
||||
|
||||
if self.target == "speech":
|
||||
if not self.speech_frames:
|
||||
# 加入静音片段(潜在的语音片段)
|
||||
self.speech_frames.extend(self.silence_frames)
|
||||
|
||||
# 加入语音片段
|
||||
self.speech_frames.extend(frames)
|
||||
|
||||
speech_bytes = bytes(self.speech_frames)
|
||||
|
||||
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
|
||||
|
||||
if self.target == "speech":
|
||||
if not self.speech_frames:
|
||||
# 如果之前没有语音片段,则将当前帧加入静音片段
|
||||
self.silence_frames.extend(frames)
|
||||
# 确保静音片段长度不超过 1s
|
||||
self.silence_frames = self.silence_frames[
|
||||
-1 * 1 * 2 * self.sample_rate :
|
||||
]
|
||||
else:
|
||||
# 如果之前有语音片段,则将当前帧加入语音片段
|
||||
self.speech_frames.extend(frames)
|
||||
|
||||
if (
|
||||
self.target == "silence"
|
||||
and self.silence_count > self.min_silence_duration * self.sample_rate / 1000
|
||||
):
|
||||
self.pause()
|
||||
EventManager.on_silence()
|
||||
|
||||
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
|
||||
|
||||
# 检测是否是语音
|
||||
speech_prob = Silero.vad(frames, self.sample_rate) or 0
|
||||
is_speech = speech_prob >= self.threshold
|
||||
if is_speech:
|
||||
self._handle_speech_frame(frames)
|
||||
else:
|
||||
self._handle_silence_frame(frames)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
|
||||
VAD = _VAD()
|
||||
@@ -0,0 +1,102 @@
|
||||
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
|
||||
|
||||
|
||||
class _Silero:
|
||||
def __init__(self) -> None:
|
||||
self.model = OnnxWrapper(
|
||||
path=get_model_file_path("silero_vad.onnx"),
|
||||
)
|
||||
|
||||
def vad(self, frames, sample_rate):
|
||||
try:
|
||||
audio_int16 = np.frombuffer(frames, dtype=np.int16)
|
||||
audio_float32 = audio_int16.astype(np.float32) / 32768.0
|
||||
return self.model(audio_float32, sample_rate).item()
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
Silero = _Silero()
|
||||
@@ -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,11 +20,11 @@ class EventType:
|
||||
"""事件类型"""
|
||||
SCHEDULE_EVENT = "schedule_event"
|
||||
AUDIO_INPUT_READY_EVENT = "audio_input_ready_event"
|
||||
AUDIO_OUTPUT_READY_EVENT = "audio_output_ready_event"
|
||||
|
||||
class AudioConfig:
|
||||
"""音频配置"""
|
||||
SAMPLE_RATE = 24000
|
||||
FORMAT = 8
|
||||
SAMPLE_RATE = 16000
|
||||
CHANNELS = 1
|
||||
FRAME_DURATION = 60 # ms
|
||||
FRAME_SIZE = int(SAMPLE_RATE * (FRAME_DURATION / 1000))
|
||||
@@ -1,14 +1,12 @@
|
||||
import asyncio
|
||||
import json
|
||||
import logging
|
||||
|
||||
import websockets
|
||||
|
||||
|
||||
from xiaozhi.ref import get_xiaozhi
|
||||
from xiaozhi.services.protocols.protocol import Protocol
|
||||
from xiaozhi.utils.config_manager import ConfigManager
|
||||
|
||||
|
||||
logger = logging.getLogger("WebsocketProtocol")
|
||||
from xiaozhi.services.protocols.typing import DeviceState
|
||||
from xiaozhi.utils.config import ConfigManager
|
||||
|
||||
|
||||
class WebsocketProtocol(Protocol):
|
||||
@@ -17,7 +15,11 @@ class WebsocketProtocol(Protocol):
|
||||
# 获取配置管理器实例
|
||||
self.config = ConfigManager.instance()
|
||||
self.websocket = None
|
||||
self.server_sample_rate = 16000
|
||||
self.server_sample_rate = 24000
|
||||
self.server_frame_duration = 60
|
||||
self.server_frame_size = int(
|
||||
self.server_sample_rate * (self.server_frame_duration / 1000)
|
||||
)
|
||||
self.connected = False
|
||||
self.hello_received = None # 初始化时先设为 None
|
||||
self.WEBSOCKET_URL = self.config.get_config("NETWORK.WEBSOCKET_URL")
|
||||
@@ -27,9 +29,20 @@ class WebsocketProtocol(Protocol):
|
||||
self.CLIENT_ID = self.config.get_client_id()
|
||||
self.DEVICE_ID = self.config.get_device_id()
|
||||
|
||||
async def _close_websocket(self):
|
||||
if self.websocket:
|
||||
try:
|
||||
await self.websocket.close()
|
||||
self.websocket = None
|
||||
self.connected = False
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def connect(self) -> bool:
|
||||
"""连接到WebSocket服务器"""
|
||||
try:
|
||||
await self._close_websocket()
|
||||
|
||||
# 在连接时创建 Event,确保在正确的事件循环中
|
||||
self.hello_received = asyncio.Event()
|
||||
|
||||
@@ -41,17 +54,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 +80,12 @@ 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,40 +99,29 @@ 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连接已关闭")
|
||||
except Exception:
|
||||
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):
|
||||
async def send_audio(self, frames: list[bytes]):
|
||||
"""发送音频数据"""
|
||||
if not self.is_audio_channel_opened(): # 使用已有的 is_connected 方法
|
||||
return
|
||||
|
||||
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)}")
|
||||
for frame in frames:
|
||||
await self.websocket.send(frame)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
async def send_text(self, message: str):
|
||||
"""发送文本消息"""
|
||||
@@ -138,24 +129,18 @@ class WebsocketProtocol(Protocol):
|
||||
try:
|
||||
await self.websocket.send(message)
|
||||
except Exception as e:
|
||||
await self.close_audio_channel()
|
||||
if self.on_network_error:
|
||||
self.on_network_error(f"发送消息失败: {str(e)}")
|
||||
|
||||
raise e
|
||||
def is_audio_channel_opened(self) -> bool:
|
||||
"""检查音频通道是否打开"""
|
||||
return self.websocket is not None and self.connected
|
||||
|
||||
async def open_audio_channel(self) -> bool:
|
||||
"""建立 WebSocket 连接
|
||||
_is_heartbeat_running = False
|
||||
|
||||
如果尚未连接,则创建新的 WebSocket 连接
|
||||
Returns:
|
||||
bool: 连接是否成功
|
||||
"""
|
||||
if not self.connected:
|
||||
return await self.connect()
|
||||
return True
|
||||
async def open_audio_channel(self):
|
||||
if not self._is_heartbeat_running:
|
||||
self._is_heartbeat_running = True
|
||||
asyncio.create_task(self.heartbeat())
|
||||
await self.connect()
|
||||
|
||||
async def _handle_server_hello(self, data: dict):
|
||||
"""处理服务器的 hello 消息
|
||||
@@ -169,23 +154,24 @@ class WebsocketProtocol(Protocol):
|
||||
# 验证传输方式
|
||||
transport = data.get("transport")
|
||||
if not transport or transport != "websocket":
|
||||
logger.error(f"不支持的传输方式: {transport}")
|
||||
return
|
||||
|
||||
# TODO 使用默认的 24k 采样率
|
||||
# xiaozhi-esp32-server 返回的参数是 16k 采样率,但实际用的是 24k 采样率
|
||||
|
||||
# 获取音频参数
|
||||
audio_params = data.get("audio_params")
|
||||
if audio_params:
|
||||
# 获取服务器的采样率
|
||||
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} 不一致,"
|
||||
"重采样后可能会失真"
|
||||
)
|
||||
# audio_params = data.get("audio_params")
|
||||
# if audio_params:
|
||||
# # 获取服务器的采样率
|
||||
# sample_rate = audio_params.get("sample_rate")
|
||||
# if sample_rate:
|
||||
# self.server_sample_rate = sample_rate
|
||||
# frame_duration = audio_params.get("frame_duration")
|
||||
# if frame_duration:
|
||||
# self.server_frame_duration = frame_duration
|
||||
# self.server_frame_size = int(
|
||||
# self.server_sample_rate * (self.server_frame_duration / 1000)
|
||||
# )
|
||||
|
||||
# 设置 hello 接收事件
|
||||
self.hello_received.set()
|
||||
@@ -194,10 +180,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 +193,17 @@ 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
|
||||
|
||||
async def heartbeat(self):
|
||||
while True:
|
||||
if self.websocket and get_xiaozhi().device_state == DeviceState.IDLE:
|
||||
try:
|
||||
await self.send_text(
|
||||
json.dumps({"session_id": "", "type": "ping"})
|
||||
)
|
||||
except Exception:
|
||||
# 发送心跳失败,重新连接
|
||||
await self.open_audio_channel()
|
||||
await asyncio.sleep(1)
|
||||
@@ -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)
|
||||
@@ -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服务器,请检查网络连接!")
|
||||
@@ -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)
|
||||
@@ -0,0 +1,115 @@
|
||||
import argparse
|
||||
import asyncio
|
||||
import threading
|
||||
|
||||
import numpy as np
|
||||
import open_xiaoai_server
|
||||
|
||||
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")
|
||||
|
||||
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("🔥 唤醒小爱")
|
||||
EventManager.on_interrupt()
|
||||
elif text and line.get("payload", {}).get("is_final"):
|
||||
print(f"🔥 收到指令: {text}")
|
||||
await EventManager.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()
|
||||
@@ -0,0 +1,483 @@
|
||||
import asyncio
|
||||
import json
|
||||
import re
|
||||
import threading
|
||||
import time
|
||||
|
||||
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,
|
||||
DeviceState,
|
||||
EventType,
|
||||
ListeningMode,
|
||||
)
|
||||
from xiaozhi.services.protocols.websocket_protocol import WebsocketProtocol
|
||||
from xiaozhi.utils.base import get_env
|
||||
from xiaozhi.utils.config import ConfigManager
|
||||
from xiaozhi.xiaoai import XiaoAI
|
||||
|
||||
|
||||
class XiaoZhi:
|
||||
"""智能音箱应用程序主类"""
|
||||
|
||||
_instance = None
|
||||
|
||||
@classmethod
|
||||
def instance(cls):
|
||||
"""获取单例实例"""
|
||||
if cls._instance is None:
|
||||
cls._instance = XiaoZhi()
|
||||
return cls._instance
|
||||
|
||||
def __init__(self):
|
||||
"""初始化应用程序"""
|
||||
# 确保单例模式
|
||||
if XiaoZhi._instance is not None:
|
||||
raise Exception("XiaoZhi是单例类,请使用instance()获取实例")
|
||||
XiaoZhi._instance = self
|
||||
|
||||
# 获取配置管理器实例
|
||||
self.config = ConfigManager.instance()
|
||||
|
||||
# 状态变量
|
||||
self.device_state = DeviceState.IDLE
|
||||
self.voice_detected = False
|
||||
self.current_text = ""
|
||||
self.current_emotion = "neutral"
|
||||
|
||||
# 音频处理相关
|
||||
self.audio_codec = None
|
||||
|
||||
# 事件循环和线程
|
||||
self.loop = asyncio.new_event_loop()
|
||||
self.loop_thread = None
|
||||
self.running = False
|
||||
|
||||
# 任务队列和锁
|
||||
self.main_tasks = []
|
||||
self.mutex = threading.Lock()
|
||||
|
||||
# 协议实例
|
||||
self.protocol = None
|
||||
|
||||
# 回调函数
|
||||
self.on_state_changed_callbacks = []
|
||||
|
||||
# 初始化事件对象
|
||||
self.events = {
|
||||
EventType.SCHEDULE_EVENT: threading.Event(),
|
||||
EventType.AUDIO_INPUT_READY_EVENT: threading.Event(),
|
||||
}
|
||||
|
||||
# 创建显示界面
|
||||
self.display = None
|
||||
set_xiaozhi(self)
|
||||
|
||||
def run(self):
|
||||
self.protocol = WebsocketProtocol()
|
||||
|
||||
# 创建并启动事件循环线程
|
||||
self.loop_thread = threading.Thread(target=self._run_event_loop)
|
||||
self.loop_thread.daemon = True
|
||||
self.loop_thread.start()
|
||||
|
||||
# 等待事件循环准备就绪
|
||||
time.sleep(0.1)
|
||||
|
||||
# 初始化应用程序
|
||||
asyncio.run_coroutine_threadsafe(XiaoAI.init_xiaoai(), self.loop)
|
||||
asyncio.run_coroutine_threadsafe(self._initialize_xiaozhi(), self.loop)
|
||||
|
||||
# 启动主循环线程
|
||||
main_loop_thread = threading.Thread(target=self._main_loop)
|
||||
main_loop_thread.daemon = True
|
||||
main_loop_thread.start()
|
||||
|
||||
VAD.start()
|
||||
KWS.start()
|
||||
|
||||
# 启动 GUI
|
||||
self._initialize_display()
|
||||
self.display.start()
|
||||
|
||||
def _run_event_loop(self):
|
||||
"""运行事件循环的线程函数"""
|
||||
asyncio.set_event_loop(self.loop)
|
||||
self.loop.run_forever()
|
||||
|
||||
async def _initialize_xiaozhi(self):
|
||||
"""初始化应用程序组件"""
|
||||
|
||||
# 初始化音频编解码器
|
||||
self._initialize_audio()
|
||||
|
||||
# 设置协议回调
|
||||
self.protocol.on_network_error = self._on_network_error
|
||||
self.protocol.on_incoming_audio = self._on_incoming_audio
|
||||
self.protocol.on_incoming_json = self._on_incoming_json
|
||||
self.protocol.on_audio_channel_opened = self._on_audio_channel_opened
|
||||
self.protocol.on_audio_channel_closed = self._on_audio_channel_closed
|
||||
|
||||
# 打开音频通道
|
||||
self.device_state = DeviceState.CONNECTING
|
||||
await self.protocol.open_audio_channel()
|
||||
|
||||
def _initialize_audio(self):
|
||||
"""初始化音频设备和编解码器"""
|
||||
try:
|
||||
from xiaozhi.services.audio.codec import AudioCodec
|
||||
|
||||
self.audio_codec = AudioCodec()
|
||||
except Exception as e:
|
||||
self.alert("错误", f"初始化音频设备失败: {e}")
|
||||
|
||||
threading.Thread(target=self._audio_input_event_trigger, daemon=True).start()
|
||||
|
||||
def _initialize_display(self):
|
||||
"""初始化显示界面"""
|
||||
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(
|
||||
press_callback=self.start_listening,
|
||||
release_callback=self.stop_listening,
|
||||
status_callback=self._get_status_text,
|
||||
text_callback=self._get_current_text,
|
||||
emotion_callback=self._get_current_emotion,
|
||||
mode_callback=self._on_mode_changed,
|
||||
auto_callback=self.toggle_chat_state,
|
||||
abort_callback=lambda: self.abort_speaking(AbortReason.WAKE_WORD_DETECTED),
|
||||
)
|
||||
|
||||
def _main_loop(self):
|
||||
"""应用程序主循环"""
|
||||
self.running = True
|
||||
|
||||
while self.running:
|
||||
# 等待事件
|
||||
for event_type, event in self.events.items():
|
||||
if event.is_set():
|
||||
event.clear()
|
||||
|
||||
if event_type == EventType.AUDIO_INPUT_READY_EVENT:
|
||||
self._handle_input_audio()
|
||||
elif event_type == EventType.SCHEDULE_EVENT:
|
||||
self._process_scheduled_tasks()
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
def _process_scheduled_tasks(self):
|
||||
"""处理调度任务"""
|
||||
with self.mutex:
|
||||
tasks = self.main_tasks.copy()
|
||||
self.main_tasks.clear()
|
||||
|
||||
for task in tasks:
|
||||
try:
|
||||
task()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def schedule(self, callback):
|
||||
"""调度任务到主循环"""
|
||||
with self.mutex:
|
||||
# 如果是中止语音的任务,检查是否已经存在相同类型的任务
|
||||
if "abort_speaking" in str(callback):
|
||||
# 如果已经有中止任务在队列中,就不再添加
|
||||
if any("abort_speaking" in str(task) for task in self.main_tasks):
|
||||
return
|
||||
self.main_tasks.append(callback)
|
||||
self.events[EventType.SCHEDULE_EVENT].set()
|
||||
|
||||
def _handle_input_audio(self):
|
||||
"""处理音频输入"""
|
||||
if self.device_state != DeviceState.LISTENING:
|
||||
return
|
||||
|
||||
encoded_data = self.audio_codec.read_audio()
|
||||
if encoded_data and self.protocol and self.protocol.is_audio_channel_opened():
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.protocol.send_audio(encoded_data), self.loop
|
||||
)
|
||||
|
||||
def _on_network_error(self, message):
|
||||
"""网络错误回调"""
|
||||
self.set_device_state(DeviceState.IDLE)
|
||||
if self.device_state != DeviceState.CONNECTING:
|
||||
self.set_device_state(DeviceState.IDLE)
|
||||
|
||||
# 关闭现有连接
|
||||
if self.protocol:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.protocol.close_audio_channel(), self.loop
|
||||
)
|
||||
|
||||
def _on_incoming_audio(self, data):
|
||||
"""接收音频数据回调"""
|
||||
if self.device_state == DeviceState.SPEAKING:
|
||||
self.audio_codec.write_audio(data)
|
||||
|
||||
def _on_incoming_json(self, json_data):
|
||||
"""接收JSON数据回调"""
|
||||
try:
|
||||
if not json_data:
|
||||
return
|
||||
|
||||
# 解析JSON数据
|
||||
if isinstance(json_data, str):
|
||||
data = json.loads(json_data)
|
||||
else:
|
||||
data = json_data
|
||||
|
||||
# 处理不同类型的消息
|
||||
msg_type = data.get("type", "")
|
||||
if msg_type == "tts":
|
||||
self._handle_tts_message(data)
|
||||
elif msg_type == "stt":
|
||||
self._handle_stt_message(data)
|
||||
elif msg_type == "llm":
|
||||
self._handle_llm_message(data)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def _handle_tts_message(self, data):
|
||||
"""处理TTS消息"""
|
||||
state = data.get("state", "")
|
||||
if state == "start":
|
||||
EventManager.on_tts_start(data.get("session_id"))
|
||||
self.schedule(lambda: self._handle_tts_start())
|
||||
elif state == "stop":
|
||||
EventManager.on_tts_end(data.get("session_id"))
|
||||
self.schedule(lambda: self._handle_tts_stop())
|
||||
elif state == "sentence_start":
|
||||
text = data.get("text", "")
|
||||
if text:
|
||||
print(f"🤖 小智:{text}")
|
||||
|
||||
verification_code = re.search(r"验证码.*?(\d+)", text) or re.search(
|
||||
r"控制面板.*?(\d+)", text
|
||||
)
|
||||
if verification_code:
|
||||
self.config.update_config_file(
|
||||
"VERIFICATION_CODE", verification_code.group(1)
|
||||
)
|
||||
|
||||
self.schedule(lambda: self.set_chat_message("assistant", text))
|
||||
|
||||
def _handle_tts_start(self):
|
||||
"""处理TTS开始事件"""
|
||||
if (
|
||||
self.device_state == DeviceState.IDLE
|
||||
or self.device_state == DeviceState.LISTENING
|
||||
):
|
||||
self.set_device_state(DeviceState.SPEAKING)
|
||||
|
||||
def _handle_tts_stop(self):
|
||||
"""处理TTS停止事件"""
|
||||
pass
|
||||
|
||||
def _handle_stt_message(self, data):
|
||||
"""处理STT消息"""
|
||||
text = data.get("text", "")
|
||||
if text:
|
||||
print(f"💬 我说:{text}")
|
||||
self.schedule(lambda: self.set_chat_message("user", text))
|
||||
|
||||
def _handle_llm_message(self, data):
|
||||
"""处理LLM消息"""
|
||||
emotion = data.get("emotion", "")
|
||||
if emotion:
|
||||
self.schedule(lambda: self.set_emotion(emotion))
|
||||
|
||||
async def _on_audio_channel_opened(self):
|
||||
"""音频通道打开回调"""
|
||||
self.set_device_state(DeviceState.IDLE)
|
||||
|
||||
def _audio_input_event_trigger(self):
|
||||
"""音频输入事件触发器"""
|
||||
while self.running:
|
||||
if self.audio_codec.input_stream.is_active():
|
||||
self.events[EventType.AUDIO_INPUT_READY_EVENT].set()
|
||||
time.sleep(0.01)
|
||||
|
||||
async def _on_audio_channel_closed(self):
|
||||
"""音频通道关闭回调"""
|
||||
self.set_device_state(DeviceState.IDLE)
|
||||
self.audio_codec.stop_streams()
|
||||
|
||||
def set_device_state(self, state):
|
||||
"""设置设备状态"""
|
||||
self.device_state = state
|
||||
|
||||
VAD.pause() # 停用 VAD
|
||||
self.audio_codec.stop_streams() # 停用输入输出流
|
||||
|
||||
if state == DeviceState.IDLE:
|
||||
self.display.update_status("待命")
|
||||
self.display.update_emotion("😶")
|
||||
elif state == DeviceState.CONNECTING:
|
||||
self.display.update_status("连接中...")
|
||||
elif state == DeviceState.LISTENING:
|
||||
self.display.update_status("聆听中...")
|
||||
self.display.update_emotion("🙂")
|
||||
# 停止输出流
|
||||
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.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:
|
||||
pass
|
||||
|
||||
def _get_status_text(self):
|
||||
"""获取当前状态文本"""
|
||||
states = {
|
||||
DeviceState.IDLE: "待命",
|
||||
DeviceState.CONNECTING: "连接中...",
|
||||
DeviceState.LISTENING: "聆听中...",
|
||||
DeviceState.SPEAKING: "说话中...",
|
||||
}
|
||||
return states.get(self.device_state, "未知")
|
||||
|
||||
def _get_current_text(self):
|
||||
"""获取当前显示文本"""
|
||||
return self.current_text
|
||||
|
||||
def _get_current_emotion(self):
|
||||
"""获取当前表情"""
|
||||
emotions = {
|
||||
"neutral": "😶",
|
||||
"happy": "🙂",
|
||||
"laughing": "😆",
|
||||
"funny": "😂",
|
||||
"sad": "😔",
|
||||
"angry": "😠",
|
||||
"crying": "😭",
|
||||
"loving": "😍",
|
||||
"embarrassed": "😳",
|
||||
"surprised": "😲",
|
||||
"shocked": "😱",
|
||||
"thinking": "🤔",
|
||||
"winking": "😉",
|
||||
"cool": "😎",
|
||||
"relaxed": "😌",
|
||||
"delicious": "🤤",
|
||||
"kissy": "😘",
|
||||
"confident": "😏",
|
||||
"sleepy": "😴",
|
||||
"silly": "😜",
|
||||
"confused": "🙄",
|
||||
}
|
||||
return emotions.get(self.current_emotion, "😶")
|
||||
|
||||
def set_chat_message(self, role, message):
|
||||
"""设置聊天消息"""
|
||||
self.current_text = message
|
||||
# 更新显示
|
||||
if self.display:
|
||||
self.display.update_text(message)
|
||||
|
||||
def set_emotion(self, emotion):
|
||||
"""设置表情"""
|
||||
self.current_emotion = emotion
|
||||
# 更新显示
|
||||
if self.display:
|
||||
self.display.update_emotion(self._get_current_emotion())
|
||||
|
||||
def start_listening(self):
|
||||
"""开始监听"""
|
||||
self.schedule(self._start_listening_impl)
|
||||
|
||||
def _start_listening_impl(self):
|
||||
"""开始监听的实现"""
|
||||
if not self.protocol:
|
||||
return
|
||||
|
||||
self.set_device_state(DeviceState.IDLE)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.protocol.send_abort_speaking(AbortReason.ABORT),
|
||||
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):
|
||||
"""停止监听"""
|
||||
self.schedule(self._stop_listening_impl)
|
||||
|
||||
def _stop_listening_impl(self):
|
||||
"""停止监听的实现"""
|
||||
asyncio.run_coroutine_threadsafe(self.protocol.send_stop_listening(), self.loop)
|
||||
self.set_device_state(DeviceState.IDLE)
|
||||
|
||||
def abort_speaking(self, reason):
|
||||
"""中止语音输出"""
|
||||
self.set_device_state(DeviceState.IDLE)
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.protocol.send_abort_speaking(AbortReason.ABORT),
|
||||
self.loop,
|
||||
)
|
||||
|
||||
def alert(self, title, message):
|
||||
"""显示警告信息"""
|
||||
if self.display:
|
||||
self.display.update_text(f"{title}: {message}")
|
||||
|
||||
def on_state_changed(self, callback):
|
||||
"""注册状态变化回调"""
|
||||
self.on_state_changed_callbacks.append(callback)
|
||||
|
||||
def shutdown(self):
|
||||
"""关闭应用程序"""
|
||||
self.running = False
|
||||
|
||||
# 关闭音频编解码器
|
||||
if self.audio_codec:
|
||||
self.audio_codec.close()
|
||||
|
||||
# 关闭协议
|
||||
if self.protocol:
|
||||
asyncio.run_coroutine_threadsafe(
|
||||
self.protocol.close_audio_channel(), self.loop
|
||||
)
|
||||
|
||||
# 停止事件循环
|
||||
if self.loop and self.loop.is_running():
|
||||
self.loop.call_soon_threadsafe(self.loop.stop)
|
||||
|
||||
# 等待事件循环线程结束
|
||||
if self.loop_thread and self.loop_thread.is_alive():
|
||||
self.loop_thread.join(timeout=1.0)
|
||||
|
||||
def toggle_chat_state(self):
|
||||
"""切换聊天状态"""
|
||||
pass
|
||||
|
||||
def _on_mode_changed(self, auto_mode):
|
||||
"""处理对话模式变更"""
|
||||
pass
|
||||
@@ -1,9 +1,14 @@
|
||||
# 你的小米账号/密码
|
||||
#你的小米账号/密码
|
||||
MI_USER=23333333
|
||||
MI_PASS=xxxxxxxxx
|
||||
|
||||
# 你的小爱音箱名称/DID
|
||||
#你的小爱音箱名称/DID
|
||||
MI_DID=小爱音箱Pro
|
||||
|
||||
# 你的 SSH 登录密码(默认为 open-xiaoai)
|
||||
#你的 SSH 登录密码,默认为 open-xiaoai
|
||||
SSH_PASSWORD=open-xiaoai
|
||||
|
||||
#账号登录异常时,请取消下面的注释,使用 MI_TOKEN 登录
|
||||
#MI_TOKEN=你的 passToken 获取教程 👉 https://github.com/idootop/migpt-next/issues/4
|
||||
|
||||
# 获取设备异常时,请取消下面的注释,查看设备列表详情
|
||||
#MI_DEBUG=true
|
||||
|
||||
@@ -6,6 +6,7 @@ ENV TZ=Asia/Shanghai
|
||||
RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime \
|
||||
&& echo $TZ > /etc/timezone \
|
||||
&& apk add --no-cache \
|
||||
bash \
|
||||
file \
|
||||
patch \
|
||||
python3 \
|
||||
|
||||
@@ -1,40 +1,46 @@
|
||||
# OpenXiaoAI Patch
|
||||
|
||||
> [!CAUTION]
|
||||
> 刷机有风险,操作需谨慎。
|
||||
> 刷机有风险,操作需谨慎。请勿下载使用不明来历的固件!
|
||||
|
||||
小爱音箱 Pro 补丁固件制作流程:
|
||||
|
||||
- 固件提取(登录小米账号获取 OTA 链接)
|
||||
- 开启固化 SSH(支持自定义登录密码)
|
||||
- 禁用系统自动更新(系统更新后需要重新刷机打补丁)
|
||||
- 添加开机启动脚本 `/data/init.sh`(方便执行一些初始化脚本)
|
||||
|
||||
## 准备条件
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 本教程仅适用于 **小爱音箱 Pro(LX06)** 和 **Xiaomi 智能音箱 Pro(OH2P)** 这两款机型,**其他型号**的小爱音箱可能存在兼容性问题,请勿直接使用!🚨
|
||||
|
||||
|
||||
- 小爱音箱 Pro(LX06、OH2P)
|
||||
- Windows 电脑(需为 x86_64 架构,用于刷机)
|
||||
- 数据线(不能只是充电线,需要连接到电脑传输数据)
|
||||
- Type-C(适用于新款小爱音箱 Pro,无需拆机)
|
||||
- Micro USB(旧款小爱音箱 Pro 用这种,需要拆机)
|
||||
|
||||
|
||||
## 下载固件
|
||||
|
||||
你可以直接在 [Github Releases](https://github.com/idootop/open-xiaoai/releases) 页面下载打包好的固件:
|
||||
|
||||
- [Xiaomi 智能音箱 Pro v1.56.31](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.56.31)
|
||||
- [小爱音箱 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`,如需修改请自行制作固件。
|
||||
|
||||
> [!IMPORTANT]
|
||||
> 请下载和你当前小爱音箱版本一致的固件,跨版本刷机可能会出现未知错误,导致设备变砖。
|
||||
> 如果上面没有你的版本,请升级设备固件到最新版本,或者按照下面的教程自行制作固件。
|
||||
|
||||
你可以直接在 Github Release 页面下载打包好的补丁固件。
|
||||
|
||||
> 默认 SSH 登录密码为 `open-xiaoai`
|
||||
> [!CAUTION]
|
||||
> 当前支持的最新固件版本为:
|
||||
>
|
||||
> - Xiaomi 智能音箱 Pro 👉 [v1.56.31](https://github.com/idootop/open-xiaoai/releases/tag/OH2P_1.56.31)
|
||||
> - 小爱音箱 Pro 👉 [v1.88.206](https://github.com/idootop/open-xiaoai/releases/tag/LX06_1.88.206)
|
||||
>
|
||||
> 更新版本的固件可能存在变化,导致刷机失败,设备变砖,请自行评估风险。
|
||||
|
||||
## 制作固件
|
||||
|
||||
你也可以按照下面的 2 种方法,制作自定义固件。
|
||||
你可以按照下面的 2 种方法,制作自定义固件。
|
||||
|
||||
### 基础配置
|
||||
|
||||
@@ -61,13 +67,25 @@ SSH_PASSWORD=open-xiaoai
|
||||
- Docker:https://www.docker.com/get-started/
|
||||
|
||||
> [!NOTE]
|
||||
> Windows 系统推荐使用 Git Bash 终端运行。
|
||||
>
|
||||
> CMD 和 PowerShell 终端需要调整下面命令中文件(夹)的实际路径。
|
||||
> Windows 系统请在 [Git Bash](https://git-scm.com/downloads) 终端中运行以下命令。
|
||||
|
||||
> [!TIP]
|
||||
> 如果你是 Apple Silicon 芯片,请先在 Docker Desktop - Settings - General - Virtual Machine Options 中打开 Apple Virtual framework 选项,然后开启 `Use Rosetta for x86_64/amd64 emulation on Apple Silicon`
|
||||
|
||||
```shell
|
||||
# 克隆代码
|
||||
git clone https://github.com/idootop/open-xiaoai.git
|
||||
|
||||
# 进入当前项目根目录
|
||||
cd packages/client-patch
|
||||
|
||||
# 使用 Docker 进行构建
|
||||
docker run -it --rm --env-file $(pwd)/.env -v $(pwd)/assets:/app/assets -v $(pwd)/patches:/app/patches idootop/open-xiaoai:latest
|
||||
docker run -it --rm \
|
||||
--platform linux/amd64 \
|
||||
--env-file $(pwd)/.env \
|
||||
-v $(pwd)/assets:/app/assets \
|
||||
-v $(pwd)/patches:/app/patches \
|
||||
idootop/open-xiaoai:latest
|
||||
|
||||
# ✅ 打包完成,固件文件已复制到 assets 目录...
|
||||
# /app/assets/mico_all_92db90ed6_1.88.197/root-patched.squashfs
|
||||
@@ -81,6 +99,12 @@ docker run -it --rm --env-file $(pwd)/.env -v $(pwd)/assets:/app/assets -v $(pwd
|
||||
- Node.js 22.x: https://nodejs.org/zh-cn/download
|
||||
|
||||
```bash
|
||||
# 克隆代码
|
||||
git clone https://github.com/idootop/open-xiaoai.git
|
||||
|
||||
# 进入当前项目根目录
|
||||
cd packages/client-patch
|
||||
|
||||
# 安装依赖
|
||||
npm install
|
||||
|
||||
@@ -93,3 +117,16 @@ npm run build
|
||||
> [!TIP]
|
||||
> 如果你想要更进一步的定制自己的固件,可以参考 `src/build.sh` 脚本里的构建流程:在提取固件后自行修改固件内的脚本、配置和应用程序,然后重新打包即可。
|
||||
|
||||
## 高级选项
|
||||
|
||||
### 1. 自定义启动脚本
|
||||
|
||||
默认修改后的补丁固件,会将 `/data/init.sh` 文件作为启动脚本,开机时自动运行。如果你需要自定义开机启动脚本,可自行创建和修改该文件。
|
||||
|
||||
示例:
|
||||
|
||||
```bash
|
||||
#!/bin/sh
|
||||
|
||||
/usr/sbin/tts_play.sh '初始化成功'
|
||||
```
|
||||
|
||||
@@ -8,11 +8,11 @@
|
||||
"name": "open-xiaoai-patch",
|
||||
"version": "1.0.0",
|
||||
"dependencies": {
|
||||
"@mi-gpt/miot": "^1.0.0"
|
||||
"@mi-gpt/miot": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mi-gpt/config": "1.0.0",
|
||||
"@types/node": "^22.13.11",
|
||||
"@mi-gpt/config": "1.1.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
@@ -443,9 +443,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mi-gpt/config": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mi-gpt/config/-/config-1.0.0.tgz",
|
||||
"integrity": "sha512-oo/roYiRcGiHsZESjukwPTsRFqddShTJ8gkccR5ZMYn8SySj07A0nCsEsLEfB9mLkvNY8D6PCO1NxbRDDRBBlA==",
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/@mi-gpt/config/-/config-1.1.0.tgz",
|
||||
"integrity": "sha512-o24iFINsoDuvInnMpfBQ4zCuTObbdodjXyizXmxl3yyYOMjmIAXizO/1UZ9/dv3UVTov3sjL09aCGuBeIVhvtA==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
@@ -453,12 +453,12 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mi-gpt/miot": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mi-gpt/miot/-/miot-1.0.0.tgz",
|
||||
"integrity": "sha512-smWhddHzK1du3/LW2FifBrIIyeZNx/RvTSe0Zi4v3lYZwK8NeEwomg1jBjktQCCV4Fh8OuqNbCdNbmiVXmr5kw==",
|
||||
"version": "1.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@mi-gpt/miot/-/miot-1.2.1.tgz",
|
||||
"integrity": "sha512-DAFNUsLPepXSqJk/b+zfct8NCQHZzcl7f1t4X2WPKzxmDNagkawYlb96uPbk4syvn+DSg6vqle0Wm+gjmJ4kUA==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@mi-gpt/utils": "1.0.0",
|
||||
"@mi-gpt/utils": "1.1.1",
|
||||
"axios": "^1.8.4",
|
||||
"pako": "^2.1.0"
|
||||
},
|
||||
@@ -467,22 +467,22 @@
|
||||
}
|
||||
},
|
||||
"node_modules/@mi-gpt/utils": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@mi-gpt/utils/-/utils-1.0.0.tgz",
|
||||
"integrity": "sha512-hAU/uYPNiNJgXWkQrVqRLe/9Uz8AgtoiNwcbbwQs8iXJcyq3NjcSHkkwUseJAnaQFE9iv/kvHZZsx3r2HhlKjw==",
|
||||
"version": "1.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@mi-gpt/utils/-/utils-1.1.1.tgz",
|
||||
"integrity": "sha512-FiGHV2RDBRGE/Xb9Q5H757t3ZqOpo6gL+44VXiRxQb3fQiapOpn038RAWErHJYZajYzkxP2jl8Wggsb+wp77Zg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "22.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.1.tgz",
|
||||
"integrity": "sha512-u0HuPQwe/dHrItgHHpmw3N2fYCR6x4ivMNbPHRkBVP4CvN+kiRrKHWk3i8tXiO/joPwXLMYvF9TTF0eqgHIuOw==",
|
||||
"version": "24.2.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-24.2.1.tgz",
|
||||
"integrity": "sha512-DRh5K+ka5eJic8CjH7td8QpYEV6Zo10gfRkjHCO3weqZHWDtAaSTFtl4+VMqOJ4N5jcuhZ9/l+yy8rVgw7BQeQ==",
|
||||
"dev": true,
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~6.21.0"
|
||||
"undici-types": "~7.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
@@ -883,9 +883,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "6.21.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz",
|
||||
"integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==",
|
||||
"version": "7.10.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.10.0.tgz",
|
||||
"integrity": "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag==",
|
||||
"dev": true,
|
||||
"license": "MIT"
|
||||
}
|
||||
|
||||
@@ -5,17 +5,17 @@
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"ota": "tsx --env-file=.env src/ota.ts",
|
||||
"extract": "sh src/extract.sh",
|
||||
"patch": "sh src/patch.sh",
|
||||
"squashfs": "sh src/squashfs.sh",
|
||||
"build": "sh src/build.sh"
|
||||
"extract": "bash src/extract.sh",
|
||||
"patch": "bash src/patch.sh",
|
||||
"squashfs": "bash src/squashfs.sh",
|
||||
"build": "bash src/build.sh"
|
||||
},
|
||||
"dependencies": {
|
||||
"@mi-gpt/miot": "^1.0.0"
|
||||
"@mi-gpt/miot": "^1.2.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@mi-gpt/config": "1.0.0",
|
||||
"@types/node": "^22.13.11",
|
||||
"@mi-gpt/config": "1.1.0",
|
||||
"@types/node": "^24.2.1",
|
||||
"tsx": "^4.19.3",
|
||||
"typescript": "^5.8.2"
|
||||
}
|
||||
|
||||
@@ -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,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
#!/bin/bash
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -e
|
||||
|
||||
|
||||