159 Commits
Author SHA1 Message Date
Del Wang 5d187f5d5b docs(v2): 开发中,无限期 delay 2026-01-08 09:55:30 +08:00
Del Wang 3cf6771cc9 refactor: 异步 RPC 2026-01-08 09:53:59 +08:00
Del Wang fb6c1fd9f8 refactor: 事件收发与处理 2026-01-06 18:57:14 +08:00
Del Wang 487a5b8c14 refactor: 优化推流播放与录音,支持立体声 2026-01-06 14:06:15 +08:00
Del Wang 0125a1478e refactor: 优化音频读取与编码传输 2026-01-06 12:08:06 +08:00
Del Wang c0bc4b8ae6 fix: 修复服务端推流播放异常的问题 2026-01-05 20:13:25 +08:00
Del Wang d88d63b15b chore: polish code 2026-01-04 15:06:55 +08:00
Del Wang cce3dd630a refactor: 重构 Open-XiaoAI Client V2 2026-01-04 11:24:37 +08:00
Del Wang 052ce06355 chore: polish client/server code 2026-01-04 10:42:06 +08:00
Del Wang eeb442d5b8 refactor: 分离音频管理模块 2026-01-03 19:26:35 +08:00
Del Wang 34924fa06e chore: refine client/server mod 2026-01-03 19:17:13 +08:00
Del Wang c4f7fbec6d chore: test client call server 2026-01-02 17:37:27 +08:00
Del Wang a2d06958e1 chore: 测试播放和录音 2026-01-02 17:23:24 +08:00
Del Wang 24b3fdddf8 fix(stereo): DISCOVERY_MAGIC 2026-01-02 16:25:48 +08:00
Del Wang 6f55021651 refactor: handlers 2026-01-02 15:16:31 +08:00
Del Wang 4ed8d3dca9 chore: 初始化测试场景 2026-01-02 13:04:19 +08:00
Del Wang 33da56dead fix: 修复 runtime 镜像 2026-01-02 12:50:23 +08:00
Del Wang ef28121f5d refactor: 调整模块位置 2026-01-02 10:22:16 +08:00
Del Wang 4d459afd28 chore: 初始化 client v2 2026-01-02 09:59:29 +08:00
Del Wang aa2a5d8474 chore(stereo): 优化播放设备重初始化 2026-01-01 20:27:42 +08:00
Del Wang b69bd5a8f9 fix(stereo): 修复主从节点同步异常的问题 2026-01-01 19:58:04 +08:00
Del Wang f4340f068f chore: 更新固件版本 2026-01-01 17:12:02 +08:00
Del Wang b07e7500c1 chore: 优化开机初始化脚本 2026-01-01 17:06:10 +08:00
Del Wang 2027a61877 feat: 新增不同型号小爱音箱组立体声应用 2026-01-01 14:29:16 +08:00
Del Wang 3017473850 fix: 异步播放提示音,防止阻塞 2026-01-01 14:28:32 +08:00
Del Wang 118c34eb5c chore: 完善从节点连接提示音 2026-01-01 13:05:06 +08:00
Del WangandDel ab872180fc feat(stereo): 新增不同型号的小爱音箱组立体声应用 2025-12-31 22:53:33 +08:00
Del WangandDel 7bdbb30bd2 chore: 清空 apps 目录 2025-12-31 22:53:33 +08:00
Del WangandDel 7e80d6cd74 feat: 新增小爱音箱交叉编译环境 2025-12-31 22:53:33 +08:00
Del WangandDel 2ff214c873 feat: 自动重启音频应用使配置生效/恢复 2025-12-31 22:53:33 +08:00
Del WangandDel cdb4fcff85 chore: 调整从节点等待时间间隔 2025-12-31 22:53:33 +08:00
Del WangandDel ba6eaf6674 Revert "feat: 支持多播放源并发混音"
This reverts commit 32dced2fa2.
2025-12-31 22:53:33 +08:00
Del WangandDel 41be071b60 Revert "chore: 修改间隔"
This reverts commit 0c8fbafd3f.
2025-12-31 22:53:33 +08:00
Del WangandDel 84c332d742 chore: 修改间隔 2025-12-31 22:53:33 +08:00
Del WangandDel 09c625cf32 feat: 支持多播放源并发混音 2025-12-31 22:53:33 +08:00
Del WangandDel aabe882b88 fix: 修复 master 无法正常退出的问题 2025-12-31 22:53:33 +08:00
Del WangandDel 316f61d0bb chore: 整理 AudioConfig 2025-12-31 22:53:33 +08:00
Del WangandDel 4bce9e8d23 chore: 优化 alsa 配置 2025-12-31 22:53:33 +08:00
Del WangandDel 871d01a158 fix: 修复开始播放音频时爆音的问题 2025-12-31 22:53:33 +08:00
Del WangandDel 1caef01836 fix: 支持从节点订阅不同声道 2025-12-31 22:53:33 +08:00
Del WangandDel f4b70088fe fix: 修复 master 退出环境清理流程 2025-12-31 22:53:33 +08:00
Del WangandDel 81ac343098 feat: 支持动态切换主从播放模式 2025-12-31 22:53:33 +08:00
Del WangandDel b277fb7234 chore: 优化连接提示 2025-12-31 22:53:33 +08:00
Del WangandDel 9fe7ebd65d fix: run_slave 支持主节点下线后自动重新连接 2025-12-31 22:53:33 +08:00
Del WangandDel f7dd1586e8 fix: 修复 OpusCodec::decode_loss 2025-12-31 22:53:33 +08:00
Del WangandDel 28d714a2ef refactor: 拆分网络通信模块 2025-12-31 22:53:33 +08:00
Del WangandDel 76260d412c refactor: 拆分 stereo 模块 2025-12-31 22:53:33 +08:00
Del WangandDel 05d2174873 refactor: 使用 UDP 传输音频流 2025-12-31 22:53:33 +08:00
Del WangandDel fbe64f63dd chore: 从设备使用默认输出 2025-12-31 22:53:33 +08:00
Del WangandDel 518436d000 chore: 直接读取本地 /etc/asound.conf 配置文件 2025-12-31 22:53:33 +08:00
Del WangandDel 9b2f61b037 chore: 优化代码 2025-12-31 22:53:33 +08:00
Del WangandDel bd9565577f feat: 优化立体声同步播放 2025-12-31 22:53:33 +08:00
Del WangandDel 962647afea feat: 多设备组立体声 2025-12-31 22:53:33 +08:00
Del WangandDel 8e788f4b47 feat: 优化音频传输参数 2025-12-31 22:53:33 +08:00
Del WangandDel 392d8bc21b feat: 完成立体声 demo 的 client / server 构建 2025-12-31 22:53:33 +08:00
Del WangandDel 06645dc563 feat: 立体声 client 构建 2025-12-31 22:53:33 +08:00
Del WangandDel 61ecb07a99 chore: 拆分 bin 入口 2025-12-31 22:53:33 +08:00
Del WangandDel 3d9d852bda perf: 优化产物体积 2025-12-31 22:53:33 +08:00
Del WangandDel fba40393c2 feat: 动态链接到 opus 和 alsa 基础库,完成音频录制播放和编解码 2025-12-31 22:53:33 +08:00
Del WangandDel c521e274e9 feat: 新增测试 runtime 2025-12-31 22:53:33 +08:00
DelandGitHub 70a64e1eee Merge pull request #90 from hehetoshang/main
ci: 添加 Rust 客户端编译工作流
2025-12-31 21:55:35 +08:00
hehetoshang 77a19c41e8 Merge branch 'main' of https://github.com/hehetoshang/open-xiaoai 2025-12-31 20:10:15 +08:00
hehetoshang b7292dbd12 ci: 添加 Rust 客户端编译工作流
添加用于编译 client-rust 的 GitHub Actions 工作流,当 main 分支的 client-rust 代码变更时自动触发。工作流包含 Rust 工具链安装、交叉编译和二进制文件上传步骤。
2025-12-31 20:10:05 +08:00
Del Wang 0f75a10b9f chore: 更新 xiaozhi 自启动初始化脚本 2025-12-10 18:27:26 +08:00
Del Wang 2d6c17d02f fix: 修复自启动脚本网络连接状态检测 2025-12-10 18:16:51 +08:00
DelandGitHub 871a04ef79 Merge pull request #66 from x1a0b0/refactor
Some refactors
2025-10-13 17:45:29 +08:00
x1a0b0 7b473ad86a Eliminate several singletons
just for better lifecycle management

Signed-off-by: x1a0b0 <xbtseng@gmail.com>
2025-10-13 14:20:11 +08:00
x1a0b0 61943b16ac Apply clippy lint
Signed-off-by: x1a0b0 <xbtseng@gmail.com>
2025-10-13 11:34:44 +08:00
x1a0b0 d43a9588c9 Update deps
Signed-off-by: x1a0b0 <xbtseng@gmail.com>
2025-10-13 11:29:48 +08:00
3D_DLWandDel 5e70f73c1e Update sed command delimiter in patch.sh
Fix sed command to use pipe instead of slash for delimiter.
2025-09-27 14:51:21 +08:00
Del Wang 31e997a192 chore: debug ota 2025-09-01 09:49:07 +08:00
Del Wang 39b2dfe220 feat: 更新 Xiaomi 智能音箱 Pro v1.58.1 固件 2025-08-31 19:29:52 +08:00
Del Wang 066fddbb68 feat: 更新小爱音箱 Pro v1.88.221 固件 2025-08-26 09:38:04 +08:00
Del Wang b5d00db7ca fix: 开机启动延时应不低于 15s,防止设备连接后自动启动,导致连接失败 2025-08-26 09:37:43 +08:00
Del Wang 1405eca23f fix: 优化脚本 Shebang, close #56 2025-08-17 07:18:16 +08:00
Del Wang 43e6739726 chore(patch): 更新固件信息获取依赖版本 2025-08-15 12:43:06 +08:00
Zion PuandDel affd94d1cf Update README.md 2025-07-28 15:14:27 +08:00
NicodeandDel 53f8ab31ee fix: availability of using xiaozhi-esp32-server as backend 2025-06-30 10:14:00 +08:00
Del Wang c5edea969c chore: 更新配置文件 DEVICE_ID 默认值 2025-06-16 21:52:29 +08:00
Del Wang e4dc39ef39 chore: 更新 Xiaomi 智能音箱 Pro v1.56.31 固件 2025-06-08 15:42:01 +08:00
Del Wang 729ba86d43 fix: 修复启动脚本等待时长 2025-06-05 00:37:02 +08:00
Del Wang 0075f13ffd fix(xiaozhi): 网络异常后 ws 重新连接 2025-06-04 22:25:18 +08:00
Del Wang 96d69ec8a6 fix: 音频输出流采样率默认值 24k 2025-05-28 23:35:59 +08:00
Del Wang c0b11436fd fix: 自启动脚本添加等待网络连接延时 2025-05-28 23:04:28 +08:00
Del Wang 948364dcc9 fix(xiaozhi): 修复语音识别丢失前面几个字的问题 2025-05-28 23:01:20 +08:00
DelandGitHub c06bbf6c51 Merge pull request #24 from NullYing/patch-1
加入联网等待延迟
2025-05-27 23:56:52 +08:00
NullYingandGitHub f2ba7b496d Update boot.sh 2025-05-27 22:35:44 +08:00
Del Wang aa5d4490bb docs: 添加小爱音箱 Pro 内存限制相关说明 2025-05-27 20:57:24 +08:00
Del Wang e3609fc888 docs: 添加项目路径相关说明 2025-05-27 20:31:59 +08:00
Del Wang ba828b782d fix: 优化 VAD 语音片段收集 2025-05-26 23:12:35 +08:00
Del Wang bcf98c5365 chore: 调整唤醒逻辑 2025-05-26 23:12:35 +08:00
Del Wang 8e7165e636 fix: 验证码正则匹配 2025-05-26 23:12:34 +08:00
Del Wang d83c2b3093 fix: 修复 tts_end 和 abort 循环调用的问题 2025-05-26 23:12:34 +08:00
DelandGitHub 824ff6bc68 docs(xiaozhi): 添加 CLI 和 GUI 运行模式的相关说明 2025-05-22 00:19:29 +08:00
Del Wang db642fc176 docs: 更新局域网部署说明 2025-05-21 19:42:22 +08:00
Del Wang 7974ab3f2b fix: pause kws on wake up 2025-05-21 19:29:28 +08:00
Del Wang c34c884f86 docs: 更新小智 AI 演示视频链接 2025-05-21 19:12:12 +08:00
WJG c61f44e5ed refactor: silero vad 2025-05-19 22:23:11 +08:00
WJG b2eba4bb89 docs: 更新 VAD + KWS 模型文件下载地址 2025-05-19 21:21:22 +08:00
Del Wang 66d222bdf7 feat: add ASCII_BANNER 2025-05-19 00:38:02 +08:00
Del Wang 58acbc29f4 feat(xiaozhi): 新增小智 AI 一键运行 Docker 镜像 2025-05-19 00:16:49 +08:00
Del Wang 59c3280938 feat(xiaozhi): 支持在 python 端监听 event 和 runShell 2025-05-11 20:47:02 +08:00
Del Wang e687280ab1 fix: 修复自启动脚本 2025-05-11 19:13:21 +08:00
Del Wang 6bdd1b9683 docs: 更新绑定设备后需要重启生效的说明 2025-05-11 16:04:27 +08:00
Del Wang c8e7861bdb fix: ota version 2025-05-11 15:23:23 +08:00
Del Wang 8fc5f073b5 docs: 更新 xiaozhi-esp32-server 连接教程 2025-05-11 15:16:22 +08:00
Del Wang 6eca122f24 chore(xiaozhi): 更新配置文件 2025-05-11 15:12:15 +08:00
Del Wang 9a8e07a68e docs: update readme 2025-05-11 11:46:12 +08:00
Del Wang cde047af19 feat(migpt): 新增 Docker 一键运行镜像 2025-05-11 11:34:42 +08:00
Del Wang a73ef08074 fix: echo server address 2025-05-10 17:15:40 +08:00
Del Wang cf853bb07b docs(kws): 更新适配机型的相关说明 2025-05-10 13:22:17 +08:00
Del Wang 9129afcc99 feat: 新增 kws 调试脚本 2025-05-10 12:55:19 +08:00
Del Wang a2c9cd5b70 docs: 更新启动脚本开机自启动相关说明 2025-05-10 02:26:36 +08:00
Del Wang b4f52ee5b9 feat: 新增 client 一键安装脚本 2025-05-10 01:41:58 +08:00
Del Wang 73bf1c6f4d docs: 更新小爱音箱自定义唤醒词演示视频 2025-05-10 01:05:32 +08:00
Del Wang e39675abb8 docs: 更新唤醒词识别常见问题说明 2025-05-09 23:39:43 +08:00
Del Wang 95caae8aa4 feat: 完成小爱音箱自定义唤醒词 2025-05-08 23:31:01 +08:00
Del Wang 8d5faa28dc fix: kill 2025-05-08 23:15:44 +08:00
Del Wang f8d4599cc4 fix: kws monitor 2025-05-08 23:13:41 +08:00
Del Wang e9e4ca92dd fix: kill kws 2025-05-08 23:08:44 +08:00
Del Wang 3f85e07ba7 fix: 确保 kws 正确启动 2025-05-08 23:07:07 +08:00
Del Wang ca728e3dfc feat: 新增 client-kws 2025-05-08 22:38:30 +08:00
Del Wang fc9409e55f chore: ignore temp dir 2025-05-08 00:59:54 +08:00
Del Wang 3aa0cf673b chore: 更新默认唤醒词 2025-05-08 00:50:55 +08:00
Del Wang e3732a7f1c chore: kws 测试 2025-05-08 00:24:55 +08:00
Del Wang 62c7c88eab feat: 新增 KwsMonitor 2025-05-07 23:08:32 +08:00
Del Wang 20c21ca609 feat: 添加自定义唤醒词相关教程 2025-05-06 23:33:11 +08:00
Del Wang a74d92b4b6 docs: 添加 SSH 链接异常的相关说明 2025-05-06 21:12:13 +08:00
Del Wang 4a0fde4724 docs: 添加连接不上设备的相关说明 2025-05-06 21:03:09 +08:00
Del Wang 755ba2f202 chore: /data/open-xiaoai 2025-05-06 20:54:57 +08:00
Del Wang 767b7c4548 docs: 添加开机启动脚本的相关说明 2025-05-06 20:34:23 +08:00
Del Wang 2cb5300577 docs: 添加交叉编译相关说明 2025-05-06 20:26:05 +08:00
Del Wang 2ebfb2241a fix: 修复启动脚本未生效的问题 2025-05-06 08:27:18 +08:00
Del Wang c3cac9b2b4 chore: 调整项目演示程序目录结构 2025-04-26 21:46:32 +08:00
Del Wang 39febdfe57 docs: 更新使用说明 2025-04-26 19:11:16 +08:00
Del Wang 012a123fdc fix: 更新 Gemini AI 系统 prompt 2025-04-26 19:07:46 +08:00
Del Wang e53bc65715 feat: 新增 Gemini Live API 接入演示 2025-04-26 19:00:56 +08:00
Del Wang 0900b4cf2a fix: 修复默认录音和播放音频参数 2025-04-26 15:01:49 +08:00
Del Wang 4e7b6f6026 fix: change default sample rate to 16khz 2025-04-26 14:16:48 +08:00
Del Wang 10c464a4e9 fix: 修配置文件 deepMerge 异常的问题 2025-04-21 16:19:03 +08:00
Del Wang 487848d9b0 docs: 更新参数说明 2025-04-19 10:34:38 +08:00
Del Wang 66dc6ef741 docs: 更新刷机成功后重启设备的相关说明 2025-04-19 10:26:40 +08:00
Del Wang b63a8f95a6 docs: 更新 callAIKeywords 参数说明 2025-04-19 10:16:20 +08:00
Del Wang 9ab2ec4058 fix: /usr/sbin/tts_play.sh 2025-04-19 10:12:18 +08:00
Del Wang 1bdea51c3a docs: 刷机成功后需要重启小爱音箱 2025-04-18 23:55:36 +08:00
Del Wang 9506c83223 fix: uname -s 2025-04-18 22:13:03 +08:00
Del Wang 29f4f77c7a feat: 添加 macOS 专用小爱音箱刷机工具 2025-04-14 23:39:18 +08:00
Del Wang b7716e8654 feat: 小爱音箱 Pro v1.88.206 2025-04-14 22:48:41 +08:00
Del Wang 8838087fd7 fix: patch asound.conf.dts 2025-04-14 22:40:27 +08:00
Del Wang 6d04185792 fix: patch asound.conf.dts 2025-04-14 22:37:37 +08:00
Del Wang e0f4dba612 fix: 修复 LX06 声卡补丁 2025-04-14 21:25:28 +08:00
Del Wang 3280809844 docs: 小爱音箱 Pro 声卡补丁有问题,暂时请勿下载/制作 2025-04-14 10:11:57 +08:00
Del Wang 80b5ae4db7 docs: remove UTM 2025-04-13 22:33:35 +08:00
Del Wang 8f85d2dd56 docs: add banner image 2025-04-13 20:03:34 +08:00
Del Wang 89019c7125 docs: 新增相关项目推荐和参考链接 2025-04-13 19:21:37 +08:00
Del Wang a863da3067 feat: 开放小爱音箱 Pro 刷机教程 2025-04-13 18:57:41 +08:00
Del Wang aebf94d5cd docs: 请勿下载使用不明来历的固件! 2025-04-13 13:04:54 +08:00
Del Wang 949dee9500 docs: 更新补丁固件下载注意事项 2025-04-13 13:03:39 +08:00
Del Wang 05072facfd chore: no false 2025-04-13 12:28:48 +08:00
206 changed files with 14707 additions and 2470 deletions
+30
View File
@@ -0,0 +1,30 @@
# 排除全部文件
**/*
# Rust
!packages/client-rust/src
!packages/client-rust/Cargo.toml
!packages/client-rust/Cargo.lock
# MiGPT
!examples/migpt/src
!examples/migpt/migpt
!examples/migpt/config.ts
!examples/migpt/package.json
!examples/migpt/pnpm-lock.yaml
!examples/migpt/Cargo.toml
!examples/migpt/Cargo.lock
!examples/migpt/tsconfig.json
examples/migpt/migpt/open-xiaoai.node
# XiaoZhi
!examples/xiaozhi/src
!examples/xiaozhi/xiaozhi
!examples/xiaozhi/Cargo.toml
!examples/xiaozhi/Cargo.lock
!examples/xiaozhi/config.py
!examples/xiaozhi/main.py
!examples/xiaozhi/uv.lock
!examples/xiaozhi/.python-version
!examples/xiaozhi/requirements.txt
!examples/xiaozhi/pyproject.toml
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
name: 小爱音箱 Pro v${{ steps.build.outputs.version }}
tag: ${{ steps.build.outputs.tag }}
body: 小爱音箱 Pro v${{ steps.build.outputs.version }} 固件更新
draft: true
draft: false
prerelease: false
makeLatest: latest
removeArtifacts: true
+1 -1
View File
@@ -36,7 +36,7 @@ jobs:
name: Xiaomi 智能音箱 Pro v${{ steps.build.outputs.version }}
tag: ${{ steps.build.outputs.tag }}
body: Xiaomi 智能音箱 Pro v${{ steps.build.outputs.version }} 固件更新
draft: true
draft: false
prerelease: false
makeLatest: latest
removeArtifacts: true
+41
View File
@@ -0,0 +1,41 @@
name: Compile Client Rust
on:
push:
branches: [ main ]
paths:
- 'packages/client-rust/**'
pull_request:
branches: [ main ]
paths:
- 'packages/client-rust/**'
jobs:
compile:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
override: true
components: rustfmt, clippy
target: armv7-unknown-linux-gnueabihf
- name: Install cross
run: cargo install cross
- name: Compile client-rust
working-directory: packages/client-rust
run: cross build --release --target armv7-unknown-linux-gnueabihf
- name: Upload compiled binary
uses: actions/upload-artifact@v4
with:
name: client-rust-binary
path: packages/client-rust/target/armv7-unknown-linux-gnueabihf/release/client
retention-days: 7
+2
View File
@@ -14,6 +14,7 @@ node_modules
# Testing
coverage
temp
# Turbo
.turbo
@@ -38,6 +39,7 @@ yarn-error.log*
.venv
.mypy_cache
__pycache__
*.egg-info
# Misc
.DS_Store
+45 -21
View File
@@ -2,6 +2,8 @@
让小爱音箱「听见你的声音」,解锁无限可能。
![](./docs/images/cover.jpg)
## 简介
2017 年,当全球首款千万级销量的智能音箱诞生时,我们以为触摸到了未来。但很快发现,这些设备被困在「指令-响应」的牢笼里:
@@ -10,7 +12,7 @@
- 它能执行命令,却不会主动思考
- 它有千万用户,却只有一套思维
我们曾幻想中的"贾维斯"级人工智能,在现实场景中沦为"闹钟+音乐播放器"。
我们曾幻想中的"贾维斯"级人工智能,在现实场景中沦为"天气预报+音乐播放器"。
**真正的智能不应被预设的代码逻辑所束缚,而应像生命体般在交互中进化。**
@@ -24,46 +26,68 @@
## 你的声音 + 小爱音箱 = 无限可能
👉 [小爱音箱接入小智 AI 演示视频](https://www.bilibili.com/video/BV1NBXWYSEvX)
👉 [小爱音箱接入小智 AI 演示视频](https://www.bilibili.com/video/BV1TxJhzvEhz)
[![](./assets/images/xiaozhi.jpg)](https://www.bilibili.com/video/BV1NBXWYSEvX)
[![](./docs/images/xiaozhi.jpg)](https://www.bilibili.com/video/BV1TxJhzvEhz)
👉 [小爱音箱自定义唤醒词演示视频](https://www.bilibili.com/video/BV1YfVUz5EMj)
[![](./docs/images/kws.jpg)](https://www.bilibili.com/video/BV1YfVUz5EMj)
👉 [小爱音箱接入 MiGPT 演示视频](https://www.bilibili.com/video/BV1N1421y7qn)
[![](./assets/images/migpt.jpg)](https://www.bilibili.com/video/BV1N1421y7qn)
[![](./docs/images/migpt.jpg)](https://www.bilibili.com/video/BV1N1421y7qn)
## 刷机教程
## 快速开始
> [!CAUTION]
> 刷机有风险,操作需谨慎。
整理中,敬请期待 :)
## 使用说明
> [!IMPORTANT]
> 本教程仅适用于 **小爱音箱 ProLX06** 和 **Xiaomi 智能音箱 ProOH2P** 这两款机型,**其他型号**的小爱音箱请勿直接使用!🚨
本项目由 Client 端 + Server 端两部分组成,你可以按照以下顺序运行该项目:
1. 按照刷机教程更新小爱音箱固件,开启 SSH 连接到小爱音箱(整理教程中……
2.你的电脑上编译好 Rust 补丁程序(Client 端),然后复制到小爱音箱上运行 👉 [教程](packages/client-rust/README.md)
3. 在你的电脑上运行 Server 端演示程序,体验小爱音箱的全新能力 ✨
- Python Server 演示 👉 [小爱音箱接入小智 AI](packages/server-python/README.md)
- Node.js Server 演示 👉 [小爱音箱接入 MiGPT-Next](packages/server-node/README.md)
1. 刷机更新小爱音箱补丁固件,开启 SSH 连接到小爱音箱 👉 [教程](docs/flash.md)
2.小爱音箱上安装运行 Client 端补丁程序 👉 [教程](packages/client-rust/README.md)
3. 运行以下演示程序,体验小爱音箱的全新能力 ✨
- 👉 [小爱音箱接入小智 AI](examples/xiaozhi/README.md)
- 👉 [小爱音箱自定义唤醒词](examples/kws/README.md)
- 👉 [小爱音箱接入 MiGPT(完美版)](examples/migpt/README.md)
- 👉 [小爱音箱接入 Gemini Live API](examples/gemini/README.md)
- 👉 [小爱音箱组立体声(支持不同型号机型)](examples/stereo/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),否则请立即终止使用并彻底删除本项目。
## License
[MIT](LICENSE) License © 2024-PRESENT Del Wang
MIT License © 2024-PRESENT [Del Wang](https://del.wang)
+2 -2
View File
@@ -4,11 +4,11 @@
## 授权范围
本项目授权范围仅限于技术原理研究、安全漏洞验证及非营利性个人使用。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
本项目仅供学术研究或个人测试用途。严禁用于商业服务、网络攻击、数据窃取、系统破坏等违反《网络安全法》及使用者所在地司法管辖区的法律规定的场景。
## 非官方声明
本项目为非盈利开源项目,由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务等知识产权均归属权利方所有。若权利方主张权益,用户应立即终止使用并彻底销毁项目副本。
本项目为开源非营利项目,由第三方开发者独立开发,与小米集团及其关联方(下称"权利方")无任何隶属/合作关系,未获其官方授权/认可或技术支持。项目中涉及的商标、固件、云服务等知识产权均归属权利方所有。若权利方主张权益,用户应立即终止使用并彻底销毁项目副本。
## 风险告知
+115
View File
@@ -0,0 +1,115 @@
# 小爱音箱 Pro 刷机教程
> [!IMPORTANT]
> 本教程仅适用于 **小爱音箱 ProLX06** 和 **Xiaomi 智能音箱 ProOH2P** 这两款机型,**其他型号**的小爱音箱请勿直接使用!🚨
### 准备条件
- 小爱音箱
- Xiaomi 智能音箱 ProOH2P
- 小爱音箱 ProLX06
- 数据线
- Type-C(适用于新款小爱音箱 Pro,**无需拆机**)
- Micro USB(旧款小爱音箱 Pro 用这种,**需要拆机**)
> [!NOTE]
> 注意:数据线不能只是充电线,需要连接到电脑上能传输数据才行。
### 连接电脑
新款的 Xiaomi 智能音箱 Pro 可以直接使用 Type-C 数据线通过音响底部的 Type-C 接口连接到电脑。
旧款小爱音箱 Pro 需要先把音箱外壳拆下来([参考步骤](https://www.52audio.com/archives/38303.html)),然后在主板左上角找到 Mico USB 的调试接口,使用 Mico USB 数据线连接到电脑。
![](./images/mico-usb.jpg)
### 开始刷机
> [!CAUTION]
> 刷机有风险,操作需谨慎。刷机可能会造成设备失去保修资格,变砖无法运行等。请自行评估相关风险,一切后果自负!🚨
首先,下载或自行制作[补丁固件](../packages/client-patch/README.md),重命名为 `root_patched.squashfs`
然后,下载刷机工具:https://androidmtk.com/download-amlogic-flash-tool
把下载好的刷机工具文件夹解压到桌面,重命名为 `Amlogic_Flash_Tool_v6.0.0`
> [!TIP]
> 如果你是 macOS 系统,请到此处下载[刷机工具和使用教程](../packages/flash-tool/README.md)。
![](./images/driver.jpg)
然后运行 `AMLLogic driver installer.exe` 文件,按照提示安装 USB 驱动。
![](./images/bash.jpg)
安装好驱动后,打开 `/bin` 目录,然后在空白处右键选择使用 [Git Bash](https://git-scm.com/downloads) 打开当前目录。
> [!NOTE]
> 请确认你已经将小爱音箱用数据线连接到了 Windows 电脑,并且安装好了驱动程序。
然后,请按照说明在打开的终端里依次执行以下命令:
```shell
# 🚗 第 1 步:拔掉小爱音箱的电源线,重新插上电源后,立即在终端执行以下命令
./update.exe identify
# 如果显示类似下面的内容,说明小爱音箱已经成功连接到了电脑,可以继续进行下一步
# This firmware version is 0-7-0-16-0-0-0-0
# 否则继续快速重试几次该命令,或者重新拔掉电源上电后重试,直至显示出来版本号。
# ⏰ 第 2 步:设置启动分区和等待时长
# 设置 uboot 启动等待 15s
./update.exe bulkcmd "setenv bootdelay 15"
# 设置 boot0 作为启动分区
./update.exe bulkcmd "setenv boot_part boot0"
# 保存环境变量,使生效
./update.exe bulkcmd "saveenv"
# ⚡️ 第 3 步:刷入 system0
./update.exe partition system0 root_patched.squashfs
# 注意:需要把 root_patched.squashfs 替换成你下载的补丁固件实际路径
```
> [!TIP]
> 如果你卡在第一步,连接不上设备,可以按照[此教程](https://github.com/idootop/open-xiaoai/issues/6#issuecomment-2815632879)排查问题。
> [!NOTE]
> 提示刷机成功之后,拔掉数据线和电源,重新插电重启小爱音箱即可。
> 如果重启之后小爱音箱没有反应,可以拔掉电源等几分钟再重新上电开机。
> 如果还是没反应,可以重新刷机试试看,或者将启动分区设置成 `boot1` 恢复原系统启动。
### SSH 连接到小爱音箱
刷机成功后,补丁固件默认开启 SSH 功能,默认密码 `open-xiaoai`
```shell
ssh -o HostKeyAlgorithms=+ssh-rsa root@你的小爱音箱局域网IP地址
# 比如:ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.227
```
> [!TIP]
> 如果刷机之后 SSH 连接不上去小爱音箱,请检查:
> - 你设置的启动分区(boot0)是否和你刷写的系统分区(system0)一致(修改启动分区之后,需要 `saveenv` 才会保存生效)
> - 你的电脑和小爱音箱是否处在同一网络环境下(可以更换路由器试试看)
> - 你的内网防火墙是否屏蔽了 22 端口(SSH 默认连接端口)
> - 如果你是自己构建的补丁固件,请检查 SSH 相关的 [patch](../packages/client-patch/patches/01-ssh.patch) 是否生效
### 更新系统
小爱音箱有 2 套系统,上面我们只是将 `system0` 刷成了打补丁后的系统。
如果之后你想要更新系统,或者换回原来的系统,将启动分支设置成 `boot1` 重启即可。
>
```shell
# 连接到 SSH 后执行
fw_env -s boot_part boot1
# 或者使用和上面刷机相同的方式
./update.exe bulkcmd "setenv boot_part boot1"
./update.exe bulkcmd "saveenv"
```
Binary file not shown.

After

Width:  |  Height:  |  Size: 352 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 525 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 342 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Before

Width:  |  Height:  |  Size: 627 KiB

After

Width:  |  Height:  |  Size: 627 KiB

Before

Width:  |  Height:  |  Size: 130 KiB

After

Width:  |  Height:  |  Size: 130 KiB

@@ -13,7 +13,7 @@ pyo3-async-runtimes = { version = "0.24", features = [
"attributes",
"tokio-runtime",
] }
open-xiaoai = { path = "../client-rust" }
open-xiaoai = { path = "../../packages/client-rust" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
serde = { version = "1.0", features = ["derive"] }
+39
View File
@@ -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` 文件中的密钥。
## 环境准备
为了能够正常编译运行该项目,你需要安装以下依赖环境/工具:
- uvhttps://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 生态自行实现。
+4
View File
@@ -0,0 +1,4 @@
from .gemini import Gemini
from .xiaoai import XiaoAi
__all__ = ["Gemini", "XiaoAi"]
+104
View File
@@ -0,0 +1,104 @@
import os
from typing import Awaitable, Callable, Optional
from google import genai
from google.genai import types
GEMINI_MODEL = "gemini-2.0-flash-live-001"
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY") or "你的 API KEY"
class Gemini:
running = False
client = genai.Client(api_key=GEMINI_API_KEY)
session: Optional[genai.live.AsyncSession] = None
config = types.LiveConnectConfig(
response_modalities=[types.Modality.AUDIO],
system_instruction=types.Content(
parts=[types.Part(text="你是小爱音箱,请用中文回答用户的问题。")]
),
speech_config=types.SpeechConfig(
language_code="cmn-CN",
voice_config=types.VoiceConfig(
prebuilt_voice_config=types.PrebuiltVoiceConfig(voice_name="Leda")
),
),
context_window_compression=(
types.ContextWindowCompressionConfig(
sliding_window=types.SlidingWindow(),
)
),
)
@classmethod
async def send_text(cls, text: str):
if not cls.session:
return
await cls.session.send_client_content(
turns={"role": "user", "parts": [{"text": text}]},
turn_complete=True,
)
@classmethod
async def send_audio(cls, data: bytes):
if not cls.session:
return
await cls.session.send_realtime_input(
audio=types.Blob(data=data, mime_type="audio/pcm;rate=16000")
)
@classmethod
def stop(cls):
cls.running = False
if cls.session:
cls.session.close()
@classmethod
async def start(
cls,
on_audio: Optional[Callable[[bytes], Awaitable[None]]] = None,
on_text: Optional[Callable[[str], Awaitable[None]]] = None,
set_is_speaking: Optional[Callable[[bool], Awaitable[None]]] = None,
):
if cls.running:
return
cls.running = True
async with cls.client.aio.live.connect(
model=GEMINI_MODEL, config=cls.config
) as session:
cls.session = session
print("🔊 AI: ", "session connected")
while True:
print("🔊 AI: ", "waiting for response")
if not cls.running:
break
if set_is_speaking:
await set_is_speaking(False)
async for response in session.receive():
if response.server_content is None:
continue
if response.server_content.interrupted is True:
continue
if set_is_speaking:
await set_is_speaking(True)
if response.data is not None:
print("🔊 AI: ", len(response.data))
if on_audio:
await on_audio(response.data)
if response.text is not None:
print("✅ AI: ", response.text)
if on_text:
await on_text(response.text)
+51
View File
@@ -0,0 +1,51 @@
import asyncio
import numpy as np
import open_xiaoai_server
from gemini import Gemini
class XiaoAi:
loop: asyncio.AbstractEventLoop
is_ai_speaking = False
speaking_count = 0
@classmethod
async def start(cls):
cls.loop = asyncio.get_event_loop()
open_xiaoai_server.register_fn("on_input_data", cls.input_audio)
await open_xiaoai_server.start_server()
@classmethod
async def output_audio(cls, data: bytes):
await open_xiaoai_server.on_output_data(data)
@classmethod
def input_audio(cls, data: bytes):
if cls.is_ai_speaking:
# 如果 AI 正在回答问题,则不发送音频,防止把 AI 的声音被当做输入录制进来
# 暂不支持中断 AI 的回复,需要等待 AI 回答完成后才能重新响应用户的语音输入
return
async def send_audio_task():
audio_array = np.frombuffer(data, dtype=np.uint16)
await Gemini.send_audio(audio_array.tobytes())
asyncio.run_coroutine_threadsafe(send_audio_task(), cls.loop)
@classmethod
async def set_is_speaking(cls, is_speaking: bool):
if is_speaking:
cls.speaking_count += 1
cls.is_ai_speaking = True
return
# 延迟 1 秒,如果 AI 还在说话,则设置为不说话
async def set_is_speaking_task():
speaking_count = cls.speaking_count
await asyncio.sleep(1)
if cls.speaking_count == speaking_count:
cls.is_ai_speaking = False
asyncio.run_coroutine_threadsafe(set_is_speaking_task(), cls.loop)
+19
View File
@@ -0,0 +1,19 @@
import asyncio
from gemini.gemini import Gemini
from gemini.xiaoai import XiaoAi
async def main():
xiaoai_task = asyncio.create_task(XiaoAi.start())
gemini_task = asyncio.create_task(
Gemini.start(
on_audio=XiaoAi.output_audio,
set_is_speaking=XiaoAi.set_is_speaking,
)
)
await asyncio.gather(xiaoai_task, gemini_task)
if __name__ == "__main__":
asyncio.run(main())
+29
View File
@@ -0,0 +1,29 @@
[project]
name = "open-xiaoai-gemini"
version = "1.0.0"
description = "小爱音箱接入 Gemini Live API"
readme = "README.md"
requires-python = ">=3.12"
dependencies = [
"asyncio>=3.4.3",
"google-genai>=1.12.1",
"httpx[socks]>=0.28.1",
"maturin>=1.8.3",
"numpy>=2.2.3",
]
[build-system]
requires = ["maturin>=1.0,<2.0"]
build-backend = "maturin"
[tool.maturin]
features = ["pyo3/extension-module"]
[tool.uv]
# Rebuild package when any rust files change
# https://github.com/PyO3/maturin/issues/2314#issuecomment-2488542771
cache-keys = [
{ file = "pyproject.toml" },
{ file = "Cargo.toml" },
{ file = "src/**/*.rs" },
]
@@ -1,4 +1,5 @@
use open_xiaoai::base::{AppError, VERSION};
use open_xiaoai::services::audio::config::AudioConfig;
use open_xiaoai::services::connect::data::{Event, Request, Response, Stream};
use open_xiaoai::services::connect::handler::MessageHandler;
use open_xiaoai::services::connect::message::{MessageManager, WsStream};
@@ -19,10 +20,34 @@ async fn test() -> Result<(), AppError> {
SpeakerManager::play_text("已连接").await?;
let _ = RPC::instance()
.call_remote("start_recording", None, None)
.call_remote(
"start_recording",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 16000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
let _ = RPC::instance().call_remote("start_play", None, None).await;
let _ = RPC::instance()
.call_remote(
"start_play",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 24000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
Ok(())
}
+434
View File
@@ -0,0 +1,434 @@
version = 1
revision = 1
requires-python = ">=3.12"
[[package]]
name = "annotated-types"
version = "0.7.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643 },
]
[[package]]
name = "anyio"
version = "4.9.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "idna" },
{ name = "sniffio" },
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916 },
]
[[package]]
name = "asyncio"
version = "3.4.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/da/54/054bafaf2c0fb8473d423743e191fcdf49b2c1fd5e9af3524efbe097bafd/asyncio-3.4.3.tar.gz", hash = "sha256:83360ff8bc97980e4ff25c964c7bd3923d333d177aa4f7fb736b019f26c7cb41", size = 204411 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/22/74/07679c5b9f98a7cb0fc147b1ef1cc1853bc07a4eb9cb5731e24732c5f773/asyncio-3.4.3-py3-none-any.whl", hash = "sha256:c4d18b22701821de07bd6aea8b53d21449ec0ec5680645e5317062ea21817d2d", size = 101767 },
]
[[package]]
name = "cachetools"
version = "5.5.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/6c/81/3747dad6b14fa2cf53fcf10548cf5aea6913e96fab41a3c198676f8948a5/cachetools-5.5.2.tar.gz", hash = "sha256:1a661caa9175d26759571b2e19580f9d6393969e5dfca11fdb1f947a23e640d4", size = 28380 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/72/76/20fa66124dbe6be5cafeb312ece67de6b61dd91a0247d1ea13db4ebb33c2/cachetools-5.5.2-py3-none-any.whl", hash = "sha256:d26a22bcc62eb95c3beabd9f1ee5e820d3d2704fe2967cbe350e20c8ffcd3f0a", size = 10080 },
]
[[package]]
name = "certifi"
version = "2025.1.31"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1c/ab/c9f1e32b7b1bf505bf26f0ef697775960db7932abeb7b516de930ba2705f/certifi-2025.1.31.tar.gz", hash = "sha256:3d5da6925056f6f18f119200434a4780a94263f10d1c21d032a6f6b2baa20651", size = 167577 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/16/b0/572805e227f01586461c80e0fd25d65a2115599cc9dad142fee4b747c357/charset_normalizer-3.4.1.tar.gz", hash = "sha256:44251f18cd68a75b56585dd00dae26183e102cd5e0f9f1466e6df5da2ed64ea3", size = 123188 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/9a/dd1e1cdceb841925b7798369a09279bd1cf183cef0f9ddf15a3a6502ee45/charset_normalizer-3.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:73d94b58ec7fecbc7366247d3b0b10a21681004153238750bb67bd9012414545", size = 196105 },
{ url = "https://files.pythonhosted.org/packages/d3/8c/90bfabf8c4809ecb648f39794cf2a84ff2e7d2a6cf159fe68d9a26160467/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dad3e487649f498dd991eeb901125411559b22e8d7ab25d3aeb1af367df5efd7", size = 140404 },
{ url = "https://files.pythonhosted.org/packages/ad/8f/e410d57c721945ea3b4f1a04b74f70ce8fa800d393d72899f0a40526401f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c30197aa96e8eed02200a83fba2657b4c3acd0f0aa4bdc9f6c1af8e8962e0757", size = 150423 },
{ url = "https://files.pythonhosted.org/packages/f0/b8/e6825e25deb691ff98cf5c9072ee0605dc2acfca98af70c2d1b1bc75190d/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2369eea1ee4a7610a860d88f268eb39b95cb588acd7235e02fd5a5601773d4fa", size = 143184 },
{ url = "https://files.pythonhosted.org/packages/3e/a2/513f6cbe752421f16d969e32f3583762bfd583848b763913ddab8d9bfd4f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc2722592d8998c870fa4e290c2eec2c1569b87fe58618e67d38b4665dfa680d", size = 145268 },
{ url = "https://files.pythonhosted.org/packages/74/94/8a5277664f27c3c438546f3eb53b33f5b19568eb7424736bdc440a88a31f/charset_normalizer-3.4.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ffc9202a29ab3920fa812879e95a9e78b2465fd10be7fcbd042899695d75e616", size = 147601 },
{ url = "https://files.pythonhosted.org/packages/7c/5f/6d352c51ee763623a98e31194823518e09bfa48be2a7e8383cf691bbb3d0/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:804a4d582ba6e5b747c625bf1255e6b1507465494a40a2130978bda7b932c90b", size = 141098 },
{ url = "https://files.pythonhosted.org/packages/78/d4/f5704cb629ba5ab16d1d3d741396aec6dc3ca2b67757c45b0599bb010478/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:0f55e69f030f7163dffe9fd0752b32f070566451afe180f99dbeeb81f511ad8d", size = 149520 },
{ url = "https://files.pythonhosted.org/packages/c5/96/64120b1d02b81785f222b976c0fb79a35875457fa9bb40827678e54d1bc8/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:c4c3e6da02df6fa1410a7680bd3f63d4f710232d3139089536310d027950696a", size = 152852 },
{ url = "https://files.pythonhosted.org/packages/84/c9/98e3732278a99f47d487fd3468bc60b882920cef29d1fa6ca460a1fdf4e6/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:5df196eb874dae23dcfb968c83d4f8fdccb333330fe1fc278ac5ceeb101003a9", size = 150488 },
{ url = "https://files.pythonhosted.org/packages/13/0e/9c8d4cb99c98c1007cc11eda969ebfe837bbbd0acdb4736d228ccaabcd22/charset_normalizer-3.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e358e64305fe12299a08e08978f51fc21fac060dcfcddd95453eabe5b93ed0e1", size = 146192 },
{ url = "https://files.pythonhosted.org/packages/b2/21/2b6b5b860781a0b49427309cb8670785aa543fb2178de875b87b9cc97746/charset_normalizer-3.4.1-cp312-cp312-win32.whl", hash = "sha256:9b23ca7ef998bc739bf6ffc077c2116917eabcc901f88da1b9856b210ef63f35", size = 95550 },
{ url = "https://files.pythonhosted.org/packages/21/5b/1b390b03b1d16c7e382b561c5329f83cc06623916aab983e8ab9239c7d5c/charset_normalizer-3.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:6ff8a4a60c227ad87030d76e99cd1698345d4491638dfa6673027c48b3cd395f", size = 102785 },
{ url = "https://files.pythonhosted.org/packages/38/94/ce8e6f63d18049672c76d07d119304e1e2d7c6098f0841b51c666e9f44a0/charset_normalizer-3.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:aabfa34badd18f1da5ec1bc2715cadc8dca465868a4e73a0173466b688f29dda", size = 195698 },
{ url = "https://files.pythonhosted.org/packages/24/2e/dfdd9770664aae179a96561cc6952ff08f9a8cd09a908f259a9dfa063568/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:22e14b5d70560b8dd51ec22863f370d1e595ac3d024cb8ad7d308b4cd95f8313", size = 140162 },
{ url = "https://files.pythonhosted.org/packages/24/4e/f646b9093cff8fc86f2d60af2de4dc17c759de9d554f130b140ea4738ca6/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8436c508b408b82d87dc5f62496973a1805cd46727c34440b0d29d8a2f50a6c9", size = 150263 },
{ url = "https://files.pythonhosted.org/packages/5e/67/2937f8d548c3ef6e2f9aab0f6e21001056f692d43282b165e7c56023e6dd/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2d074908e1aecee37a7635990b2c6d504cd4766c7bc9fc86d63f9c09af3fa11b", size = 142966 },
{ url = "https://files.pythonhosted.org/packages/52/ed/b7f4f07de100bdb95c1756d3a4d17b90c1a3c53715c1a476f8738058e0fa/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:955f8851919303c92343d2f66165294848d57e9bba6cf6e3625485a70a038d11", size = 144992 },
{ url = "https://files.pythonhosted.org/packages/96/2c/d49710a6dbcd3776265f4c923bb73ebe83933dfbaa841c5da850fe0fd20b/charset_normalizer-3.4.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:44ecbf16649486d4aebafeaa7ec4c9fed8b88101f4dd612dcaf65d5e815f837f", size = 147162 },
{ url = "https://files.pythonhosted.org/packages/b4/41/35ff1f9a6bd380303dea55e44c4933b4cc3c4850988927d4082ada230273/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:0924e81d3d5e70f8126529951dac65c1010cdf117bb75eb02dd12339b57749dd", size = 140972 },
{ url = "https://files.pythonhosted.org/packages/fb/43/c6a0b685fe6910d08ba971f62cd9c3e862a85770395ba5d9cad4fede33ab/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2967f74ad52c3b98de4c3b32e1a44e32975e008a9cd2a8cc8966d6a5218c5cb2", size = 149095 },
{ url = "https://files.pythonhosted.org/packages/4c/ff/a9a504662452e2d2878512115638966e75633519ec11f25fca3d2049a94a/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:c75cb2a3e389853835e84a2d8fb2b81a10645b503eca9bcb98df6b5a43eb8886", size = 152668 },
{ url = "https://files.pythonhosted.org/packages/6c/71/189996b6d9a4b932564701628af5cee6716733e9165af1d5e1b285c530ed/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:09b26ae6b1abf0d27570633b2b078a2a20419c99d66fb2823173d73f188ce601", size = 150073 },
{ url = "https://files.pythonhosted.org/packages/e4/93/946a86ce20790e11312c87c75ba68d5f6ad2208cfb52b2d6a2c32840d922/charset_normalizer-3.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fa88b843d6e211393a37219e6a1c1df99d35e8fd90446f1118f4216e307e48cd", size = 145732 },
{ url = "https://files.pythonhosted.org/packages/cd/e5/131d2fb1b0dddafc37be4f3a2fa79aa4c037368be9423061dccadfd90091/charset_normalizer-3.4.1-cp313-cp313-win32.whl", hash = "sha256:eb8178fe3dba6450a3e024e95ac49ed3400e506fd4e9e5c32d30adda88cbd407", size = 95391 },
{ url = "https://files.pythonhosted.org/packages/27/f2/4f9a69cc7712b9b5ad8fdb87039fd89abba997ad5cbe690d1835d40405b0/charset_normalizer-3.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:b1ac5992a838106edb89654e0aebfc24f5848ae2547d22c2c3f66454daa11971", size = 102702 },
{ url = "https://files.pythonhosted.org/packages/0e/f6/65ecc6878a89bb1c23a086ea335ad4bf21a588990c3f535a227b9eea9108/charset_normalizer-3.4.1-py3-none-any.whl", hash = "sha256:d98b1668f06378c6dbefec3b92299716b931cd4e6061f3c875a71ced1780ab85", size = 49767 },
]
[[package]]
name = "google-auth"
version = "2.39.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cachetools" },
{ name = "pyasn1-modules" },
{ name = "rsa" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cb/8e/8f45c9a32f73e786e954b8f9761c61422955d23c45d1e8c347f9b4b59e8e/google_auth-2.39.0.tar.gz", hash = "sha256:73222d43cdc35a3aeacbfdcaf73142a97839f10de930550d89ebfe1d0a00cde7", size = 274834 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/ce/12/ad37a1ef86006d0a0117fc06a4a00bd461c775356b534b425f00dde208ea/google_auth-2.39.0-py2.py3-none-any.whl", hash = "sha256:0150b6711e97fb9f52fe599f55648950cc4540015565d8fbb31be2ad6e1548a2", size = 212319 },
]
[[package]]
name = "google-genai"
version = "1.12.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "google-auth" },
{ name = "httpx" },
{ name = "pydantic" },
{ name = "requests" },
{ name = "typing-extensions" },
{ name = "websockets" },
]
sdist = { url = "https://files.pythonhosted.org/packages/38/9c/c907dbea921663bb7c41f415337bedd08259d17da8d156396c7237611744/google_genai-1.12.1.tar.gz", hash = "sha256:5c7eda422360643ce602a3f6b23152470ec1039310ef40080cbe4e71237f6391", size = 167752 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/29/2c/5b454dec837328eb167e78f45a14da502af223f8b94a4824e2fd0df74f19/google_genai-1.12.1-py3-none-any.whl", hash = "sha256:7cbc1bc029712946ce41bcf80c0eaa89eb8c09c308efbbfe30fd491f402c258a", size = 165940 },
]
[[package]]
name = "h11"
version = "0.16.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[package]]
name = "httpcore"
version = "1.0.9"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "h11" },
]
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[package]]
name = "httpx"
version = "0.28.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "anyio" },
{ name = "certifi" },
{ name = "httpcore" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[package.optional-dependencies]
socks = [
{ name = "socksio" },
]
[[package]]
name = "idna"
version = "3.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442 },
]
[[package]]
name = "maturin"
version = "1.8.3"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/30/0b/3fd746cf5cfa3c8d7e20ea08c0dbc2c2c765ae051d0fc43d808a38bc9548/maturin-1.8.3.tar.gz", hash = "sha256:304762f86fd53a8031b1bf006d12572a2aa0a5235485031113195cc0152e1e12", size = 199656 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/78/80/08579d0184ba743345bbd350a3b6419510ff8a4f6e9e671f713160d41fbb/maturin-1.8.3-py3-none-linux_armv6l.whl", hash = "sha256:fa27466b627150123729b2e611f9f9cfade84d24385d72c6877f78c30de30e89", size = 7758366 },
{ url = "https://files.pythonhosted.org/packages/87/1c/00755d28ae277daa828e183c3d118e2923e8b8f0cba4ff708b15d274ac0e/maturin-1.8.3-py3-none-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:583404d20d7f1d9c8f3c18dcab9014faacabbed6be02da80062c06cd0e279554", size = 15201378 },
{ url = "https://files.pythonhosted.org/packages/58/9f/b8738dc55ba3eb149ad2686d3f9b3c24e44b7baff46cc6baa85e5dc8cf5e/maturin-1.8.3-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:f9ffdac53dfe0089cf19b597410bc552eb34c856ddb41482b243a695e8b549d3", size = 7934215 },
{ url = "https://files.pythonhosted.org/packages/18/68/300f1a279486d6f63b624a76b81f0fba82545d027127c1ca4d5ded0d1b43/maturin-1.8.3-py3-none-manylinux_2_12_i686.manylinux2010_i686.musllinux_1_1_i686.whl", hash = "sha256:7949a4a17637341f84e88f4cbf0c155998780bbb7a145ed735725b907881c0ae", size = 7791270 },
{ url = "https://files.pythonhosted.org/packages/2e/6d/bf1b8bb9a8b1d9adad242b4089794be318446142975762d04f04ffabae40/maturin-1.8.3-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl", hash = "sha256:11564fac7486313b7baf3aa4e82c20e1b20364aad3fde2ccbc4c07693c0b7e16", size = 8282824 },
{ url = "https://files.pythonhosted.org/packages/18/e9/c601de8699e546774d3f9e761eab374988f88e4ed7006afbb5ff61bb41c0/maturin-1.8.3-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:8555d8701cdba6c19c4705a22ce4d2a1814efd792f55dc5873262ff903317540", size = 7595855 },
{ url = "https://files.pythonhosted.org/packages/34/b8/82ae650e6b589289f4219a480f2b220bcf3d9391ae9ea02cc7a58ef59cfc/maturin-1.8.3-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:5b2a513468c1c9b4d1728d4b6d3d044b7c183985ef819c9ef15e373b70d99c7d", size = 7634888 },
{ url = "https://files.pythonhosted.org/packages/ac/4a/4f898a66cf4697267c447a6f240b6cbcc2dcacd3cab89735bfbd44810be1/maturin-1.8.3-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.musllinux_1_1_ppc64le.whl", hash = "sha256:2fe8fdff420cfccde127bbc3d8e40835163dcebb6d28c49fffd9aaf1e6ec1090", size = 9810616 },
{ url = "https://files.pythonhosted.org/packages/31/bd/21b0d471e6ade0801f27c33672bca4d563eb1d1e624e534f3bef8a01b1ac/maturin-1.8.3-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:22cd8b6dc490fee99a62590f914f0c04ddad1dd6dbd5c7e933b3f882b1bd78c2", size = 10923701 },
{ url = "https://files.pythonhosted.org/packages/6c/07/85c32f03ed757c0626e2fc5a9d6454988228be96a756991702a2a757691b/maturin-1.8.3-py3-none-win32.whl", hash = "sha256:2427924546c9d1079b1c0c6c5c1d380e1b8187baba4e6342cca81597a0f3d697", size = 7016502 },
{ url = "https://files.pythonhosted.org/packages/d5/62/f92a130a370dd7aca13c316844b82853647f048cfe1594a81f628ab7101f/maturin-1.8.3-py3-none-win_amd64.whl", hash = "sha256:85f2b882d8235c1c1cb0a38d382ccd5b3ba0674d99cb548d49df9342cc688e36", size = 7953286 },
{ url = "https://files.pythonhosted.org/packages/04/95/8379140838cd95472de843e982d0bf674e8dbf25a899c44e2f76b15704d9/maturin-1.8.3-py3-none-win_arm64.whl", hash = "sha256:33939aabf9a06a8a14ca6c399d32616c7e574fcca8d4ff6dcd984441051f32fb", size = 6687772 },
]
[[package]]
name = "numpy"
version = "2.2.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e1/78/31103410a57bc2c2b93a3597340a8119588571f6a4539067546cb9a0bfac/numpy-2.2.4.tar.gz", hash = "sha256:9ba03692a45d3eef66559efe1d1096c4b9b75c0986b5dff5530c378fb8331d4f", size = 20270701 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/30/182db21d4f2a95904cec1a6f779479ea1ac07c0647f064dea454ec650c42/numpy-2.2.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:a7b9084668aa0f64e64bd00d27ba5146ef1c3a8835f3bd912e7a9e01326804c4", size = 20947156 },
{ url = "https://files.pythonhosted.org/packages/24/6d/9483566acfbda6c62c6bc74b6e981c777229d2af93c8eb2469b26ac1b7bc/numpy-2.2.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:dbe512c511956b893d2dacd007d955a3f03d555ae05cfa3ff1c1ff6df8851854", size = 14133092 },
{ url = "https://files.pythonhosted.org/packages/27/f6/dba8a258acbf9d2bed2525cdcbb9493ef9bae5199d7a9cb92ee7e9b2aea6/numpy-2.2.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:bb649f8b207ab07caebba230d851b579a3c8711a851d29efe15008e31bb4de24", size = 5163515 },
{ url = "https://files.pythonhosted.org/packages/62/30/82116199d1c249446723c68f2c9da40d7f062551036f50b8c4caa42ae252/numpy-2.2.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:f34dc300df798742b3d06515aa2a0aee20941c13579d7a2f2e10af01ae4901ee", size = 6696558 },
{ url = "https://files.pythonhosted.org/packages/0e/b2/54122b3c6df5df3e87582b2e9430f1bdb63af4023c739ba300164c9ae503/numpy-2.2.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c3f7ac96b16955634e223b579a3e5798df59007ca43e8d451a0e6a50f6bfdfba", size = 14084742 },
{ url = "https://files.pythonhosted.org/packages/02/e2/e2cbb8d634151aab9528ef7b8bab52ee4ab10e076509285602c2a3a686e0/numpy-2.2.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4f92084defa704deadd4e0a5ab1dc52d8ac9e8a8ef617f3fbb853e79b0ea3592", size = 16134051 },
{ url = "https://files.pythonhosted.org/packages/8e/21/efd47800e4affc993e8be50c1b768de038363dd88865920439ef7b422c60/numpy-2.2.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7a4e84a6283b36632e2a5b56e121961f6542ab886bc9e12f8f9818b3c266bfbb", size = 15578972 },
{ url = "https://files.pythonhosted.org/packages/04/1e/f8bb88f6157045dd5d9b27ccf433d016981032690969aa5c19e332b138c0/numpy-2.2.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:11c43995255eb4127115956495f43e9343736edb7fcdb0d973defd9de14cd84f", size = 17898106 },
{ url = "https://files.pythonhosted.org/packages/2b/93/df59a5a3897c1f036ae8ff845e45f4081bb06943039ae28a3c1c7c780f22/numpy-2.2.4-cp312-cp312-win32.whl", hash = "sha256:65ef3468b53269eb5fdb3a5c09508c032b793da03251d5f8722b1194f1790c00", size = 6311190 },
{ url = "https://files.pythonhosted.org/packages/46/69/8c4f928741c2a8efa255fdc7e9097527c6dc4e4df147e3cadc5d9357ce85/numpy-2.2.4-cp312-cp312-win_amd64.whl", hash = "sha256:2aad3c17ed2ff455b8eaafe06bcdae0062a1db77cb99f4b9cbb5f4ecb13c5146", size = 12644305 },
{ url = "https://files.pythonhosted.org/packages/2a/d0/bd5ad792e78017f5decfb2ecc947422a3669a34f775679a76317af671ffc/numpy-2.2.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:1cf4e5c6a278d620dee9ddeb487dc6a860f9b199eadeecc567f777daace1e9e7", size = 20933623 },
{ url = "https://files.pythonhosted.org/packages/c3/bc/2b3545766337b95409868f8e62053135bdc7fa2ce630aba983a2aa60b559/numpy-2.2.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1974afec0b479e50438fc3648974268f972e2d908ddb6d7fb634598cdb8260a0", size = 14148681 },
{ url = "https://files.pythonhosted.org/packages/6a/70/67b24d68a56551d43a6ec9fe8c5f91b526d4c1a46a6387b956bf2d64744e/numpy-2.2.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:79bd5f0a02aa16808fcbc79a9a376a147cc1045f7dfe44c6e7d53fa8b8a79392", size = 5148759 },
{ url = "https://files.pythonhosted.org/packages/1c/8b/e2fc8a75fcb7be12d90b31477c9356c0cbb44abce7ffb36be39a0017afad/numpy-2.2.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:3387dd7232804b341165cedcb90694565a6015433ee076c6754775e85d86f1fc", size = 6683092 },
{ url = "https://files.pythonhosted.org/packages/13/73/41b7b27f169ecf368b52533edb72e56a133f9e86256e809e169362553b49/numpy-2.2.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6f527d8fdb0286fd2fd97a2a96c6be17ba4232da346931d967a0630050dfd298", size = 14081422 },
{ url = "https://files.pythonhosted.org/packages/4b/04/e208ff3ae3ddfbafc05910f89546382f15a3f10186b1f56bd99f159689c2/numpy-2.2.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bce43e386c16898b91e162e5baaad90c4b06f9dcbe36282490032cec98dc8ae7", size = 16132202 },
{ url = "https://files.pythonhosted.org/packages/fe/bc/2218160574d862d5e55f803d88ddcad88beff94791f9c5f86d67bd8fbf1c/numpy-2.2.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:31504f970f563d99f71a3512d0c01a645b692b12a63630d6aafa0939e52361e6", size = 15573131 },
{ url = "https://files.pythonhosted.org/packages/a5/78/97c775bc4f05abc8a8426436b7cb1be806a02a2994b195945600855e3a25/numpy-2.2.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:81413336ef121a6ba746892fad881a83351ee3e1e4011f52e97fba79233611fd", size = 17894270 },
{ url = "https://files.pythonhosted.org/packages/b9/eb/38c06217a5f6de27dcb41524ca95a44e395e6a1decdc0c99fec0832ce6ae/numpy-2.2.4-cp313-cp313-win32.whl", hash = "sha256:f486038e44caa08dbd97275a9a35a283a8f1d2f0ee60ac260a1790e76660833c", size = 6308141 },
{ url = "https://files.pythonhosted.org/packages/52/17/d0dd10ab6d125c6d11ffb6dfa3423c3571befab8358d4f85cd4471964fcd/numpy-2.2.4-cp313-cp313-win_amd64.whl", hash = "sha256:207a2b8441cc8b6a2a78c9ddc64d00d20c303d79fba08c577752f080c4007ee3", size = 12636885 },
{ url = "https://files.pythonhosted.org/packages/fa/e2/793288ede17a0fdc921172916efb40f3cbc2aa97e76c5c84aba6dc7e8747/numpy-2.2.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8120575cb4882318c791f839a4fd66161a6fa46f3f0a5e613071aae35b5dd8f8", size = 20961829 },
{ url = "https://files.pythonhosted.org/packages/3a/75/bb4573f6c462afd1ea5cbedcc362fe3e9bdbcc57aefd37c681be1155fbaa/numpy-2.2.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a761ba0fa886a7bb33c6c8f6f20213735cb19642c580a931c625ee377ee8bd39", size = 14161419 },
{ url = "https://files.pythonhosted.org/packages/03/68/07b4cd01090ca46c7a336958b413cdbe75002286295f2addea767b7f16c9/numpy-2.2.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:ac0280f1ba4a4bfff363a99a6aceed4f8e123f8a9b234c89140f5e894e452ecd", size = 5196414 },
{ url = "https://files.pythonhosted.org/packages/a5/fd/d4a29478d622fedff5c4b4b4cedfc37a00691079623c0575978d2446db9e/numpy-2.2.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:879cf3a9a2b53a4672a168c21375166171bc3932b7e21f622201811c43cdd3b0", size = 6709379 },
{ url = "https://files.pythonhosted.org/packages/41/78/96dddb75bb9be730b87c72f30ffdd62611aba234e4e460576a068c98eff6/numpy-2.2.4-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f05d4198c1bacc9124018109c5fba2f3201dbe7ab6e92ff100494f236209c960", size = 14051725 },
{ url = "https://files.pythonhosted.org/packages/00/06/5306b8199bffac2a29d9119c11f457f6c7d41115a335b78d3f86fad4dbe8/numpy-2.2.4-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e2f085ce2e813a50dfd0e01fbfc0c12bbe5d2063d99f8b29da30e544fb6483b8", size = 16101638 },
{ url = "https://files.pythonhosted.org/packages/fa/03/74c5b631ee1ded596945c12027649e6344614144369fd3ec1aaced782882/numpy-2.2.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:92bda934a791c01d6d9d8e038363c50918ef7c40601552a58ac84c9613a665bc", size = 15571717 },
{ url = "https://files.pythonhosted.org/packages/cb/dc/4fc7c0283abe0981e3b89f9b332a134e237dd476b0c018e1e21083310c31/numpy-2.2.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:ee4d528022f4c5ff67332469e10efe06a267e32f4067dc76bb7e2cddf3cd25ff", size = 17879998 },
{ url = "https://files.pythonhosted.org/packages/e5/2b/878576190c5cfa29ed896b518cc516aecc7c98a919e20706c12480465f43/numpy-2.2.4-cp313-cp313t-win32.whl", hash = "sha256:05c076d531e9998e7e694c36e8b349969c56eadd2cdcd07242958489d79a7286", size = 6366896 },
{ url = "https://files.pythonhosted.org/packages/3e/05/eb7eec66b95cf697f08c754ef26c3549d03ebd682819f794cb039574a0a6/numpy-2.2.4-cp313-cp313t-win_amd64.whl", hash = "sha256:188dcbca89834cc2e14eb2f106c96d6d46f200fe0200310fc29089657379c58d", size = 12739119 },
]
[[package]]
name = "open-xiaoai-gemini"
version = "1.0.0"
source = { editable = "." }
dependencies = [
{ name = "asyncio" },
{ name = "google-genai" },
{ name = "httpx", extra = ["socks"] },
{ name = "maturin" },
{ name = "numpy" },
]
[package.metadata]
requires-dist = [
{ name = "asyncio", specifier = ">=3.4.3" },
{ name = "google-genai", specifier = ">=1.12.1" },
{ name = "httpx", extras = ["socks"], specifier = ">=0.28.1" },
{ name = "maturin", specifier = ">=1.8.3" },
{ name = "numpy", specifier = ">=2.2.3" },
]
[[package]]
name = "pyasn1"
version = "0.6.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ba/e9/01f1a64245b89f039897cb0130016d79f77d52669aae6ee7b159a6c4c018/pyasn1-0.6.1.tar.gz", hash = "sha256:6f580d2bdd84365380830acf45550f2511469f673cb4a5ae3857a3170128b034", size = 145322 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/f1/d6a797abb14f6283c0ddff96bbdd46937f64122b8c925cab503dd37f8214/pyasn1-0.6.1-py3-none-any.whl", hash = "sha256:0d632f46f2ba09143da3a8afe9e33fb6f92fa2320ab7e886e2d0f7672af84629", size = 83135 },
]
[[package]]
name = "pyasn1-modules"
version = "0.4.2"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e9/e6/78ebbb10a8c8e4b61a59249394a4a594c1a7af95593dc933a349c8d00964/pyasn1_modules-0.4.2.tar.gz", hash = "sha256:677091de870a80aae844b1ca6134f54652fa2c8c5a52aa396440ac3106e941e6", size = 307892 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259 },
]
[[package]]
name = "pydantic"
version = "2.11.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "annotated-types" },
{ name = "pydantic-core" },
{ name = "typing-extensions" },
{ name = "typing-inspection" },
]
sdist = { url = "https://files.pythonhosted.org/packages/10/2e/ca897f093ee6c5f3b0bee123ee4465c50e75431c3d5b6a3b44a47134e891/pydantic-2.11.3.tar.gz", hash = "sha256:7471657138c16adad9322fe3070c0116dd6c3ad8d649300e3cbdfe91f4db4ec3", size = 785513 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b0/1d/407b29780a289868ed696d1616f4aad49d6388e5a77f567dcd2629dcd7b8/pydantic-2.11.3-py3-none-any.whl", hash = "sha256:a082753436a07f9ba1289c6ffa01cd93db3548776088aa917cc43b63f68fa60f", size = 443591 },
]
[[package]]
name = "pydantic-core"
version = "2.33.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/17/19/ed6a078a5287aea7922de6841ef4c06157931622c89c2a47940837b5eecd/pydantic_core-2.33.1.tar.gz", hash = "sha256:bcc9c6fdb0ced789245b02b7d6603e17d1563064ddcfc36f046b61c0c05dd9df", size = 434395 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/ce/3cb22b07c29938f97ff5f5bb27521f95e2ebec399b882392deb68d6c440e/pydantic_core-2.33.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:1293d7febb995e9d3ec3ea09caf1a26214eec45b0f29f6074abb004723fc1de8", size = 2026640 },
{ url = "https://files.pythonhosted.org/packages/19/78/f381d643b12378fee782a72126ec5d793081ef03791c28a0fd542a5bee64/pydantic_core-2.33.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:99b56acd433386c8f20be5c4000786d1e7ca0523c8eefc995d14d79c7a081498", size = 1852649 },
{ url = "https://files.pythonhosted.org/packages/9d/2b/98a37b80b15aac9eb2c6cfc6dbd35e5058a352891c5cce3a8472d77665a6/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:35a5ec3fa8c2fe6c53e1b2ccc2454398f95d5393ab398478f53e1afbbeb4d939", size = 1892472 },
{ url = "https://files.pythonhosted.org/packages/4e/d4/3c59514e0f55a161004792b9ff3039da52448f43f5834f905abef9db6e4a/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b172f7b9d2f3abc0efd12e3386f7e48b576ef309544ac3a63e5e9cdd2e24585d", size = 1977509 },
{ url = "https://files.pythonhosted.org/packages/a9/b6/c2c7946ef70576f79a25db59a576bce088bdc5952d1b93c9789b091df716/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9097b9f17f91eea659b9ec58148c0747ec354a42f7389b9d50701610d86f812e", size = 2128702 },
{ url = "https://files.pythonhosted.org/packages/88/fe/65a880f81e3f2a974312b61f82a03d85528f89a010ce21ad92f109d94deb/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cc77ec5b7e2118b152b0d886c7514a4653bcb58c6b1d760134a9fab915f777b3", size = 2679428 },
{ url = "https://files.pythonhosted.org/packages/6f/ff/4459e4146afd0462fb483bb98aa2436d69c484737feaceba1341615fb0ac/pydantic_core-2.33.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d5e3d15245b08fa4a84cefc6c9222e6f37c98111c8679fbd94aa145f9a0ae23d", size = 2008753 },
{ url = "https://files.pythonhosted.org/packages/7c/76/1c42e384e8d78452ededac8b583fe2550c84abfef83a0552e0e7478ccbc3/pydantic_core-2.33.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:ef99779001d7ac2e2461d8ab55d3373fe7315caefdbecd8ced75304ae5a6fc6b", size = 2114849 },
{ url = "https://files.pythonhosted.org/packages/00/72/7d0cf05095c15f7ffe0eb78914b166d591c0eed72f294da68378da205101/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:fc6bf8869e193855e8d91d91f6bf59699a5cdfaa47a404e278e776dd7f168b39", size = 2069541 },
{ url = "https://files.pythonhosted.org/packages/b3/69/94a514066bb7d8be499aa764926937409d2389c09be0b5107a970286ef81/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:b1caa0bc2741b043db7823843e1bde8aaa58a55a58fda06083b0569f8b45693a", size = 2239225 },
{ url = "https://files.pythonhosted.org/packages/84/b0/e390071eadb44b41f4f54c3cef64d8bf5f9612c92686c9299eaa09e267e2/pydantic_core-2.33.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:ec259f62538e8bf364903a7d0d0239447059f9434b284f5536e8402b7dd198db", size = 2248373 },
{ url = "https://files.pythonhosted.org/packages/d6/b2/288b3579ffc07e92af66e2f1a11be3b056fe1214aab314748461f21a31c3/pydantic_core-2.33.1-cp312-cp312-win32.whl", hash = "sha256:e14f369c98a7c15772b9da98987f58e2b509a93235582838bd0d1d8c08b68fda", size = 1907034 },
{ url = "https://files.pythonhosted.org/packages/02/28/58442ad1c22b5b6742b992ba9518420235adced665513868f99a1c2638a5/pydantic_core-2.33.1-cp312-cp312-win_amd64.whl", hash = "sha256:1c607801d85e2e123357b3893f82c97a42856192997b95b4d8325deb1cd0c5f4", size = 1956848 },
{ url = "https://files.pythonhosted.org/packages/a1/eb/f54809b51c7e2a1d9f439f158b8dd94359321abcc98767e16fc48ae5a77e/pydantic_core-2.33.1-cp312-cp312-win_arm64.whl", hash = "sha256:8d13f0276806ee722e70a1c93da19748594f19ac4299c7e41237fc791d1861ea", size = 1903986 },
{ url = "https://files.pythonhosted.org/packages/7a/24/eed3466a4308d79155f1cdd5c7432c80ddcc4530ba8623b79d5ced021641/pydantic_core-2.33.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:70af6a21237b53d1fe7b9325b20e65cbf2f0a848cf77bed492b029139701e66a", size = 2033551 },
{ url = "https://files.pythonhosted.org/packages/ab/14/df54b1a0bc9b6ded9b758b73139d2c11b4e8eb43e8ab9c5847c0a2913ada/pydantic_core-2.33.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:282b3fe1bbbe5ae35224a0dbd05aed9ccabccd241e8e6b60370484234b456266", size = 1852785 },
{ url = "https://files.pythonhosted.org/packages/fa/96/e275f15ff3d34bb04b0125d9bc8848bf69f25d784d92a63676112451bfb9/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4b315e596282bbb5822d0c7ee9d255595bd7506d1cb20c2911a4da0b970187d3", size = 1897758 },
{ url = "https://files.pythonhosted.org/packages/b7/d8/96bc536e975b69e3a924b507d2a19aedbf50b24e08c80fb00e35f9baaed8/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:1dfae24cf9921875ca0ca6a8ecb4bb2f13c855794ed0d468d6abbec6e6dcd44a", size = 1986109 },
{ url = "https://files.pythonhosted.org/packages/90/72/ab58e43ce7e900b88cb571ed057b2fcd0e95b708a2e0bed475b10130393e/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6dd8ecfde08d8bfadaea669e83c63939af76f4cf5538a72597016edfa3fad516", size = 2129159 },
{ url = "https://files.pythonhosted.org/packages/dc/3f/52d85781406886c6870ac995ec0ba7ccc028b530b0798c9080531b409fdb/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2f593494876eae852dc98c43c6f260f45abdbfeec9e4324e31a481d948214764", size = 2680222 },
{ url = "https://files.pythonhosted.org/packages/f4/56/6e2ef42f363a0eec0fd92f74a91e0ac48cd2e49b695aac1509ad81eee86a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:948b73114f47fd7016088e5186d13faf5e1b2fe83f5e320e371f035557fd264d", size = 2006980 },
{ url = "https://files.pythonhosted.org/packages/4c/c0/604536c4379cc78359f9ee0aa319f4aedf6b652ec2854953f5a14fc38c5a/pydantic_core-2.33.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:e11f3864eb516af21b01e25fac915a82e9ddad3bb0fb9e95a246067398b435a4", size = 2120840 },
{ url = "https://files.pythonhosted.org/packages/1f/46/9eb764814f508f0edfb291a0f75d10854d78113fa13900ce13729aaec3ae/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:549150be302428b56fdad0c23c2741dcdb5572413776826c965619a25d9c6bde", size = 2072518 },
{ url = "https://files.pythonhosted.org/packages/42/e3/fb6b2a732b82d1666fa6bf53e3627867ea3131c5f39f98ce92141e3e3dc1/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:495bc156026efafd9ef2d82372bd38afce78ddd82bf28ef5276c469e57c0c83e", size = 2248025 },
{ url = "https://files.pythonhosted.org/packages/5c/9d/fbe8fe9d1aa4dac88723f10a921bc7418bd3378a567cb5e21193a3c48b43/pydantic_core-2.33.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:ec79de2a8680b1a67a07490bddf9636d5c2fab609ba8c57597e855fa5fa4dacd", size = 2254991 },
{ url = "https://files.pythonhosted.org/packages/aa/99/07e2237b8a66438d9b26482332cda99a9acccb58d284af7bc7c946a42fd3/pydantic_core-2.33.1-cp313-cp313-win32.whl", hash = "sha256:ee12a7be1742f81b8a65b36c6921022301d466b82d80315d215c4c691724986f", size = 1915262 },
{ url = "https://files.pythonhosted.org/packages/8a/f4/e457a7849beeed1e5defbcf5051c6f7b3c91a0624dd31543a64fc9adcf52/pydantic_core-2.33.1-cp313-cp313-win_amd64.whl", hash = "sha256:ede9b407e39949d2afc46385ce6bd6e11588660c26f80576c11c958e6647bc40", size = 1956626 },
{ url = "https://files.pythonhosted.org/packages/20/d0/e8d567a7cff7b04e017ae164d98011f1e1894269fe8e90ea187a3cbfb562/pydantic_core-2.33.1-cp313-cp313-win_arm64.whl", hash = "sha256:aa687a23d4b7871a00e03ca96a09cad0f28f443690d300500603bd0adba4b523", size = 1909590 },
{ url = "https://files.pythonhosted.org/packages/ef/fd/24ea4302d7a527d672c5be06e17df16aabfb4e9fdc6e0b345c21580f3d2a/pydantic_core-2.33.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:401d7b76e1000d0dd5538e6381d28febdcacb097c8d340dde7d7fc6e13e9f95d", size = 1812963 },
{ url = "https://files.pythonhosted.org/packages/5f/95/4fbc2ecdeb5c1c53f1175a32d870250194eb2fdf6291b795ab08c8646d5d/pydantic_core-2.33.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7aeb055a42d734c0255c9e489ac67e75397d59c6fbe60d155851e9782f276a9c", size = 1986896 },
{ url = "https://files.pythonhosted.org/packages/71/ae/fe31e7f4a62431222d8f65a3bd02e3fa7e6026d154a00818e6d30520ea77/pydantic_core-2.33.1-cp313-cp313t-win_amd64.whl", hash = "sha256:338ea9b73e6e109f15ab439e62cb3b78aa752c7fd9536794112e14bee02c8d18", size = 1931810 },
]
[[package]]
name = "requests"
version = "2.32.3"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "certifi" },
{ name = "charset-normalizer" },
{ name = "idna" },
{ name = "urllib3" },
]
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "rsa"
version = "4.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyasn1" },
]
sdist = { url = "https://files.pythonhosted.org/packages/da/8a/22b7beea3ee0d44b1916c0c1cb0ee3af23b700b6da9f04991899d0c555d4/rsa-4.9.1.tar.gz", hash = "sha256:e7bdbfdb5497da4c07dfd35530e1a902659db6ff241e39d9953cad06ebd0ae75", size = 29034 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696 },
]
[[package]]
name = "sniffio"
version = "1.3.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 },
]
[[package]]
name = "socksio"
version = "1.0.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f8/5c/48a7d9495be3d1c651198fd99dbb6ce190e2274d0f28b9051307bdec6b85/socksio-1.0.0.tar.gz", hash = "sha256:f88beb3da5b5c38b9890469de67d0cb0f9d494b78b106ca1845f96c10b91c4ac", size = 19055 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/37/c3/6eeb6034408dac0fa653d126c9204ade96b819c936e136c5e8a6897eee9c/socksio-1.0.0-py3-none-any.whl", hash = "sha256:95dc1f15f9b34e8d7b16f06d74b8ccf48f609af32ab33c608d08761c5dcbb1f3", size = 12763 },
]
[[package]]
name = "typing-extensions"
version = "4.13.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/f6/37/23083fcd6e35492953e8d2aaaa68b860eb422b34627b13f2ce3eb6106061/typing_extensions-4.13.2.tar.gz", hash = "sha256:e6c81219bd689f51865d9e372991c540bda33a0379d5573cddb9a3a23f7caaef", size = 106967 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/8b/54/b1ae86c0973cc6f0210b53d508ca3641fb6d0c56823f288d108bc7ab3cc8/typing_extensions-4.13.2-py3-none-any.whl", hash = "sha256:a439e7c04b49fec3e5d3e2beaa21755cadbbdc391694e28ccdd36ca4a1408f8c", size = 45806 },
]
[[package]]
name = "typing-inspection"
version = "0.4.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "typing-extensions" },
]
sdist = { url = "https://files.pythonhosted.org/packages/82/5c/e6082df02e215b846b4b8c0b887a64d7d08ffaba30605502639d44c06b82/typing_inspection-0.4.0.tar.gz", hash = "sha256:9765c87de36671694a67904bf2c96e395be9c6439bb6c87b5142569dcdd65122", size = 76222 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/31/08/aa4fdfb71f7de5176385bd9e90852eaf6b5d622735020ad600f2bab54385/typing_inspection-0.4.0-py3-none-any.whl", hash = "sha256:50e72559fcd2a6367a19f7a7e610e6afcb9fac940c650290eed893d61386832f", size = 14125 },
]
[[package]]
name = "urllib3"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/aa/63/e53da845320b757bf29ef6a9062f5c669fe997973f966045cb019c3f4b66/urllib3-2.3.0.tar.gz", hash = "sha256:f8c5449b3cf0861679ce7e0503c7b44b5ec981bec0d1d3795a07f1ba96f0204d", size = 307268 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c8/19/4ec628951a74043532ca2cf5d97b7b14863931476d117c471e8e2b1eb39f/urllib3-2.3.0-py3-none-any.whl", hash = "sha256:1cee9ad369867bfdbbb48b7dd50374c0967a0bb7710050facf0dd6911440e3df", size = 128369 },
]
[[package]]
name = "websockets"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437 },
{ url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096 },
{ url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332 },
{ url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152 },
{ url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096 },
{ url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523 },
{ url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790 },
{ url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165 },
{ url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160 },
{ url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395 },
{ url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841 },
{ url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440 },
{ url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098 },
{ url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329 },
{ url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111 },
{ url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054 },
{ url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496 },
{ url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829 },
{ url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217 },
{ url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195 },
{ url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393 },
{ url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837 },
{ url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743 },
]
+157
View File
@@ -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 服务。
+106
View File
@@ -0,0 +1,106 @@
#!/bin/sh
exec > /dev/null 2>&1
cat << 'EOF'
▄▖ ▖▖▘ ▄▖▄▖
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
v1.0.0 by: https://del.wang
EOF
# 等待能够正常访问 baidu.com
while ! ping -c 1 baidu.com > /dev/null 2>&1; do
echo "🤫 等待网络连接中..."
sleep 1
done
sleep 3
echo "✅ 网络连接成功"
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
+75
View File
@@ -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
+96
View File
@@ -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
+163
View File
@@ -0,0 +1,163 @@
# /// script
# requires-python = ">=3.12"
# dependencies = [
# "pypinyin",
# ]
# [tool.uv]
# exclude-newer = "2025-05-06T00:00:00Z"
# ///
import argparse
from pathlib import Path
from typing import List, Union
from pypinyin import pinyin
from pypinyin.contrib.tone_convert import to_initials, to_finals_tone
def get_args():
parser = argparse.ArgumentParser()
parser.add_argument(
"--text",
type=str,
required=False,
default="my-keywords.txt",
help="""Path to the input texts.
Each line in the texts contains the original phrase, it might also contain some
extra items, for example, the boosting score (startting with :), the triggering
threshold (startting with #, only used in keyword spotting task) and the original
phrase (startting with @). Note: extra items will be kept in the output.
example input 1 (tokens_type = ppinyin):
小爱同学 :2.0 #0.6 @小爱同学
你好问问 :3.5 @你好问问
小艺小艺 #0.6 @小艺小艺
example output 1:
x iǎo ài t óng x ué :2.0 #0.6 @小爱同学
n ǐ h ǎo w èn w èn :3.5 @你好问问
x iǎo y ì x iǎo y ì #0.6 @小艺小艺
example input 2 (tokens_type = bpe):
HELLO WORLD :1.5 #0.4
HI GOOGLE :2.0 #0.8
HEY SIRI #0.35
example output 2:
▁HE LL O ▁WORLD :1.5 #0.4
▁HI ▁GO O G LE :2.0 #0.8
▁HE Y ▁S I RI #0.35
""",
)
parser.add_argument(
"--tokens",
type=str,
required=False,
default="tokens.txt",
help="The path to tokens.txt.",
)
parser.add_argument(
"--output",
type=str,
required=False,
default="keywords.txt",
help="Path where the encoded tokens will be written to.",
)
return parser.parse_args()
def text2token(
texts: List[str],
tokens: str,
output_ids: bool = False,
):
"""将文本转换为 token 列表"""
assert Path(tokens).is_file(), f"File not exists, {tokens}"
tokens_table = {}
with open(tokens, "r", encoding="utf-8") as f:
for line in f:
toks = line.strip().split()
assert len(toks) == 2, len(toks)
assert toks[0] not in tokens_table, f"Duplicate token: {toks} "
tokens_table[toks[0]] = int(toks[1])
texts_list: List[List[str]] = []
for txt in texts:
res = []
py = [x[0] for x in pinyin(txt)]
for x in py:
initial = to_initials(x, strict=False)
final = to_finals_tone(x, strict=False)
if initial == "" and final == "":
res.append(x)
else:
if initial != "":
res.append(initial)
if final != "":
res.append(final)
texts_list.append(res)
result: List[List[Union[int, str]]] = []
for text in texts_list:
text_list = []
contain_oov = False
for txt in text:
if txt in tokens_table:
text_list.append(tokens_table[txt] if output_ids else txt)
else:
print(
f"Can't find token {txt} in token table, check your "
f"tokens.txt see if {txt} in it. skipping text : {text}."
)
contain_oov = True
break
if contain_oov:
continue
else:
result.append(text_list)
return result
def main() -> None:
args = get_args()
texts = []
# extra information like boosting score (start with :), triggering threshold (start with #)
# original keyword (start with @)
extra_info = []
with open(args.text, "r", encoding="utf8") as f:
for line in f:
extra = []
text = []
toks = line.strip().split()
if len(toks) == 1:
text.append(toks[0])
extra.append('@'+toks[0])
else:
for tok in toks:
if tok[0] == ":" or tok[0] == "#" or tok[0] == "@":
extra.append(tok)
else:
text.append(tok)
texts.append(" ".join(text))
extra_info.append(extra)
encoded_texts = text2token(
texts,
tokens=args.tokens,
)
with open(args.output, "w", encoding="utf8") as f:
for i, txt in enumerate(encoded_texts):
txt += extra_info[i]
f.write(" ".join(txt) + "\n")
print(f"✅ 唤醒词已保存到 {args.output}")
if __name__ == "__main__":
main()
+4
View File
@@ -0,0 +1,4 @@
t iān m āo j īng l íng @天猫精灵
x iǎo d ù x iǎo d ù @小度小度
d òu b āo d òu b āo @豆包豆包
n ǐ h ǎo x iǎo zh ì @你好小智
+4
View File
@@ -0,0 +1,4 @@
天猫精灵
小度小度
豆包豆包
你好小智
+227
View File
@@ -0,0 +1,227 @@
<blk> 0
<sos/eos> 1
<unk> 2
A 3
B 4
S 5
I 6
f 7
ù 8
ǔ 9
zh 10
y 11
ī 12
sh 13
ēng 14
uè 15
p 16
iàn 17
K 18
L 19
P 20
R 21
T 22
M 23
j 24
W 25
q 26
s 27
ān 28
b 29
ā 30
l 31
íng 32
iǔ 33
ch 34
ǐ 35
ì 36
èr 37
w 38
r 39
án 40
én 41
ó 42
g 43
uǎn 44
c 45
āng 46
ǎn 47
x 48
iān 49
ōng 50
àng 51
òng 52
d 53
ài 54
iù 55
ìng 56
ū 57
m 58
àn 59
ǎ 60
í 61
z 62
k 63
ǒu 64
ūn 65
uó 66
èng 67
òu 68
ú 69
éng 70
à 71
iào 72
āo 73
ào 74
h 75
uáng 76
t 77
iáo 78
è 79
iǎn 80
er 81
n 82
èi 83
ǐng 84
uā 85
ēi 86
ǎo 87
iú 88
uàng 89
uí 90
ǜ 91
āi 92
óu 93
ià 94
ǎng 95
īn 96
uà 97
uǐ 98
ái 99
ò 100
ē 101
óng 102
uàn 103
é 104
ìn 105
ùn 106
uò 107
ún 108
uì 109
uān 110
ǐn 111
iāo 112
ōu 113
uán 114
iǎo 115
á 116
ér 117
èn 118
C 119
D 120
E 121
O 122
N 123
Z 124
ěi 125
ián 126
ēn 127
iā 128
ǚ 129
áng 130
iě 131
ǒng 132
éi 133
iāng 134
J 135
V 136
ín 137
ié 138
īng 139
ō 140
ěr 141
F 142
ué 143
iǎng 144
uō 145
G 146
ǎi 147
ěn 148
H 149
en 150
X 151
i 152
ǒ 153
uǒ 154
iè 155
iū 156
U 157
iàng 158
uāng 159
uá 160
Q 161
ě 162
e 163
iǎ 164
iáng 165
Y 166
○ 167
iē 168
uī 169
ěng 170
uài 171
áo 172
a 173
ou 174
ǔn 175
uái 176
uāi 177
ióng 178
ei 179
ń 180
iá 181
iōng 182
üè 183
uē 184
uǎng 185
uǎ 186
o 187
uǎi 188
uě 189
ǘ 190
uo 191
iǒng 192
ang 193
ia 194
u 195
üě 196
#0 197
#1 198
#2 199
#3 200
#4 201
#5 202
#6 203
#7 204
#8 205
#9 206
#10 207
#11 208
#12 209
#13 210
#14 211
#15 212
#16 213
#17 214
#18 215
#19 216
#20 217
#21 218
#22 219
#23 220
#24 221
#25 222
#26 223
#27 224
#28 225
#29 226
@@ -390,6 +390,7 @@ name = "open-xiaoai"
version = "1.0.0"
dependencies = [
"futures",
"rand",
"serde",
"serde_json",
"tokio",
@@ -480,13 +481,12 @@ checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.0"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3779b94aeb87e8bd4e834cee3650289ee9e0d5677f976ecdb6d219e5f4f6cd94"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
"zerocopy",
]
[[package]]
@@ -10,7 +10,7 @@ crate-type = ["cdylib"]
[dependencies]
neon = { version = "1.1.0-alpha.1", features = ["napi-6", "futures", "tokio"] }
open-xiaoai = { path = "../client-rust" }
open-xiaoai = { path = "../../packages/client-rust" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
serde = { version = "1.0", features = ["derive"] }
+64
View File
@@ -0,0 +1,64 @@
FROM ubuntu:22.04 AS builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
WORKDIR /app
COPY examples/migpt .
COPY packages/client-rust ./client-rust
# 更新源
RUN apt update && apt install curl build-essential -y
# 安装 Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# Create a script file sourced by both interactive and non-interactive bash shells
ENV BASH_ENV=/root/.bash_env
RUN touch "$BASH_ENV"
RUN echo '. "$BASH_ENV"' >> "$HOME/.bashrc"
RUN echo '[ -s "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"' >> "$BASH_ENV"
# nvm
RUN echo 'export NVM_DIR="$HOME/.nvm"' >> "$BASH_ENV"
RUN echo '[ -s "$NVM_DIR/nvm.sh" ] && . "$NVM_DIR/nvm.sh"' >> "$BASH_ENV"
RUN echo '[ -s "$NVM_DIR/bash_completion" ] && . "$NVM_DIR/bash_completion"' >> "$BASH_ENV"
# 安装 Node.js
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash \
&& \. $NVM_DIR/nvm.sh && \. $NVM_DIR/bash_completion \
&& nvm install 22
# 安装依赖
RUN npm install -g corepack \
&& corepack enable && corepack install \
&& pnpm install
# 构建 Rust Server
RUN sed -i 's/\.\.\/\.\.\/packages\///g' Cargo.toml \
&& pnpm build
FROM ubuntu:22.04 AS runner
WORKDIR /app
COPY --from=builder /app/migpt /app/migpt
COPY --from=builder /app/package.json /app/package.json
COPY --from=builder /app/tsconfig.json /app/tsconfig.json
COPY --from=builder /app/node_modules /app/node_modules
# 更新源
RUN apt update && apt install curl build-essential -y
ENV NVM_DIR=/root/.nvm
# 安装 Node.js
RUN curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.3/install.sh | bash \
&& bash -c "source $NVM_DIR/nvm.sh && nvm install 22"
ENTRYPOINT ["bash", "-c", "source $NVM_DIR/nvm.sh && exec \"$@\"", "--"]
CMD ["npm", "run", "start"]
+83
View File
@@ -0,0 +1,83 @@
# Open-XiaoAI x MiGPT-Next
[Open-XiaoAI](https://github.com/idootop/open-xiaoai) 的 Node.js 版 Server 端,用来演示小爱音箱接入[MiGPT](https://github.com/idootop/mi-gpt)(完美版)。
相比原版的 `MiGPT``MiGPT-Next` 项目,该版本可以完美打断小爱音箱的回复,响应延迟更低,效果更完美 👍
## 快速开始
> [!NOTE]
> 继续下面的操作之前,你需要先在小爱音箱上启动运行 Rust 补丁程序 [👉 教程](../../packages/client-rust/README.md)
首先,克隆仓库代码到本地。
```shell
# 克隆代码
git clone https://github.com/idootop/open-xiaoai.git
# 进入当前项目根目录
cd examples/migpt
```
然后把 `config.ts` 文件里的配置修改成你自己的。
```typescript
export const kOpenXiaoAIConfig = {
openai: {
model: "gpt-4.1-mini",
baseURL: "https://api.openai.com/v1",
apiKey: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
},
prompt: {
system: "你是一个智能助手,请根据用户的问题给出回答。",
},
async onMessage(engine, { text }) {
if (text === "测试") {
return { text: "你好,很高兴认识你!" };
}
},
};
```
### Docker 运行
[![Docker Image Version](https://img.shields.io/docker/v/idootop/open-xiaoai-migpt?color=%23086DCD&label=docker%20image)](https://hub.docker.com/r/idootop/open-xiaoai-migpt)
推荐使用以下命令,直接 Docker 一键运行。
```shell
docker run -it --rm -p 4399:4399 -v $(pwd)/config.ts:/app/config.ts idootop/open-xiaoai-migpt:latest
```
### 编译运行
> [!TIP]
> 如果你是一名开发者,想要修改源代码实现自己想要的功能,可以按照下面的步骤,自行编译运行该项目。
为了能够正常编译运行该项目,你需要安装以下依赖环境:
- Node.js v22.x: https://nodejs.org/zh-cn/download
- Rust: https://www.rust-lang.org/learn/get-started
准备好开发环境后,按以下步骤即可正常启动该项目。
```bash
# 启用 PNPM 包管理工具
corepack enable && corepack install
# 安装依赖
pnpm install
# 编译运行
pnpm dev
```
## 注意事项
1. 默认 Server 服务端口为 `4399`(比如 ws://192.168.31.227:4399),运行前请确保该端口未被其他程序占用。
2. 默认 Rust Server 在启动时,并没有开启小爱音箱的录音能力。
如果你需要在 Node.js 端正常接收音频输入流,或者播放音频输出流,请将 `src/server.rs` 文件中被注释掉的 `start_recording``start_play` 代码加回来,然后重新编译运行。
> [!NOTE]
> 本项目只是一个简单的演示程序,抛砖引玉。如果你想要更多的功能,比如唤醒词识别、语音转文字、连续对话等(甚至是对接 OpenAI 的 [Realtime API](https://platform.openai.com/docs/guides/realtime)),可参考本项目代码自行实现。
+72
View File
@@ -0,0 +1,72 @@
import { sleep } from "@mi-gpt/utils";
import { OpenXiaoAIConfig } from "./migpt/xiaoai.js";
export const kOpenXiaoAIConfig: OpenXiaoAIConfig = {
openai: {
/**
* 你的大模型服务提供商的接口地址
*
* 支持兼容 OpenAI 接口的大模型服务,比如:DeepSeek V3 等
*
* 注意:一般以 /v1 结尾,不包含 /chat/completions 部分
* - ✅ https://api.openai.com/v1
* - ❌ https://api.openai.com/v1/(最后多了一个 /
* - ❌ https://api.openai.com/v1/chat/completions(不需要加 /chat/completions
*/
baseURL: "https://api.openai.com/v1",
/**
* API 密钥
*/
apiKey: "sk-xxxxxxxxxxxxxxxxxxxxxxxxxxxx",
/**
* 模型名称
*/
model: "gpt-4.1-mini",
},
prompt: {
/**
* 系统提示词,如需关闭可设置为:''(空字符串)
*/
system: "你是一个智能助手,请根据用户的问题给出回答。",
},
context: {
/**
* 每次对话携带的最大历史消息数(如需关闭可设置为:0)
*/
historyMaxLength: 10,
},
/**
* 只回答以下关键词开头的消息:
*
* - 请问地球为什么是圆的?
* - 你知道世界上跑的最快的动物是什么吗?
*/
callAIKeywords: ["请", "你"],
/**
* 自定义消息回复
*/
async onMessage(engine, { text }) {
if (text === "测试播放文字") {
return { text: "你好,很高兴认识你!" };
}
if (text === "测试播放音乐") {
return { url: "https://example.com/hello.mp3" };
}
if (text === "测试其他能力") {
// 打断原来小爱的回复
await engine.speaker.abortXiaoAI();
// 播放文字
await sleep(2000); // 打断小爱后需要等待 2 秒,使其恢复运行后才能继续 TTS
await engine.speaker.play({ text: "你好,很高兴认识你!", blocking: true });
// 播放音频链接
await engine.speaker.play({ url: "https://example.com/hello.mp3" });
// 告诉 MiGPT 已经处理过这条消息了,不再使用默认的 AI 回复
return { handled: true };
}
},
};
+9
View File
@@ -0,0 +1,9 @@
import { kOpenXiaoAIConfig } from "config.js";
import { OpenXiaoAI } from "./xiaoai.js";
async function main() {
await OpenXiaoAI.start(kOpenXiaoAIConfig);
process.exit(0);
}
main();
@@ -71,7 +71,7 @@ class SpeakerManager implements ISpeaker {
const res = await this.runShell(
url
? `miplayer -f '${url}'`
: `/usr/sbin/tts_play2.sh '${text || "你好"}'`,
: `/usr/sbin/tts_play.sh '${text || "你好"}'`,
{ timeout }
);
return res?.exit_code === 0;
@@ -104,7 +104,7 @@ class SpeakerManager implements ISpeaker {
silent: boolean;
}
) {
const { silent = false } = options ?? {};
const { silent = true } = options ?? {};
const command = awake
? silent
? `ubus call pnshelper event_notify '{"src":1,"event":0}'`
@@ -6,7 +6,7 @@ import { RustServer } from "./open-xiaoai.js";
import { OpenXiaoAISpeaker } from "./speaker.js";
import { randomUUID } from "node:crypto";
type OpenXiaoAIConfig = Prettify<EngineConfig<OpenXiaoAIEngine>>;
export type OpenXiaoAIConfig = Prettify<EngineConfig<OpenXiaoAIEngine>>;
const kDefaultOpenXiaoAIConfig: OpenXiaoAIConfig = {
//
@@ -57,6 +57,9 @@ class OpenXiaoAIEngine extends MiGPTEngine {
timestamp: Date.now(),
});
}
} else if (e.event === "kws") {
const keyword = e.data;
console.log("🔥 唤醒词识别", keyword);
}
};
@@ -1,6 +1,7 @@
{
"name": "open-xiaoai-node",
"name": "open-xiaoai-migpt",
"version": "1.0.0",
"description": "小爱音箱接入 MiGPT-Next",
"private": true,
"type": "module",
"scripts": {
@@ -9,11 +10,11 @@
"dev": "pnpm build && pnpm start"
},
"dependencies": {
"@mi-gpt/engine": "1.1.1",
"@mi-gpt/utils": "1.0.0"
"@mi-gpt/engine": "1.2.1",
"@mi-gpt/utils": "1.1.1"
},
"devDependencies": {
"@mi-gpt/config": "1.0.0",
"@mi-gpt/config": "1.1.0",
"@neon-rs/cli": "0.1.82",
"@types/node": "^22.13.11",
"cargo-cp-artifact": "^0.1.9",
@@ -9,15 +9,15 @@ importers:
.:
dependencies:
'@mi-gpt/engine':
specifier: 1.2.1
version: 1.2.1
'@mi-gpt/utils':
specifier: 1.1.1
version: 1.1.1
'@mi-gpt/utils':
specifier: 1.0.0
version: 1.0.0
devDependencies:
'@mi-gpt/config':
specifier: 1.0.0
version: 1.0.0
specifier: 1.1.0
version: 1.1.0
'@neon-rs/cli':
specifier: 0.1.82
version: 0.1.82
@@ -236,28 +236,28 @@ packages:
cpu: [x64]
os: [win32]
'@mi-gpt/chat@1.0.0':
resolution: {integrity: sha512-PyyQ4Vg3s1OcUxr9GXZTpBgGCRQldhm9vGKc3UpAA4QDyVJWSP6ILhXKfFHO+UfnxDr45y9wlk5PpMcgkmPNkQ==}
'@mi-gpt/chat@1.1.1':
resolution: {integrity: sha512-C5b498nGC1ZAg6/Nl5XWneOwnUQ9CgK+0wCEqNDHYkj96YWqpbW3uGS6ubgNncpVMg/uXMoc55l1hsUF0lz+oQ==}
engines: {node: '>=16'}
'@mi-gpt/config@1.0.0':
resolution: {integrity: sha512-oo/roYiRcGiHsZESjukwPTsRFqddShTJ8gkccR5ZMYn8SySj07A0nCsEsLEfB9mLkvNY8D6PCO1NxbRDDRBBlA==}
'@mi-gpt/config@1.1.0':
resolution: {integrity: sha512-o24iFINsoDuvInnMpfBQ4zCuTObbdodjXyizXmxl3yyYOMjmIAXizO/1UZ9/dv3UVTov3sjL09aCGuBeIVhvtA==}
engines: {node: '>=16'}
'@mi-gpt/engine@1.1.1':
resolution: {integrity: sha512-TyA6JCq/yGvJI+ysFuDOwLaDUJ1fjjtxHIGB3Nyr40R0Ujb32Ssz/FoCDpbVmTKj2niebZQAGuUl7/juBuQwtw==}
'@mi-gpt/engine@1.2.1':
resolution: {integrity: sha512-EmBVS/l1xj8N54ZHRmYcV0jl9vh0QBPsyMdjwmS5h9+cdG40ItUq+shVg9ZHJrwoixKIiagYJ4lAjw+yUQ1DUQ==}
engines: {node: '>=16'}
'@mi-gpt/openai@1.0.0':
resolution: {integrity: sha512-BZI2gJkcJ4TuHcyyD5fzvFzEMlSkfkkuxpycQ/mSiMC8Z3umQbRSk2z1Tg3ho8DFxf5tYiIsFzbvWCBw2WXlwg==}
'@mi-gpt/openai@1.1.1':
resolution: {integrity: sha512-pEs9y/GMY/ZDmlFfcBuowuvyE+j3cTKeQY0cMhZ9AobXD/qGNWlP7gQXojsSbVg/D0bB2snhEiWv/X5s1EzJWg==}
engines: {node: '>=16'}
'@mi-gpt/stream@1.0.0':
resolution: {integrity: sha512-nzYKGODEhTbBUv6WGmgEY4rioXfWRvs2te99nQWjl+agmN9bT7KTbHKm8tzTDApQcQltx5BTDXrXMairmLVTYQ==}
'@mi-gpt/stream@1.1.1':
resolution: {integrity: sha512-4zd8brCqGX05tStweUWFdeE4EVpZGtFXHfvfYqvdJWccFK4PZkW3ynvJ0lc4N8SdsJo5Ml6XxOftRpZUnfGJtg==}
engines: {node: '>=16'}
'@mi-gpt/utils@1.0.0':
resolution: {integrity: sha512-hAU/uYPNiNJgXWkQrVqRLe/9Uz8AgtoiNwcbbwQs8iXJcyq3NjcSHkkwUseJAnaQFE9iv/kvHZZsx3r2HhlKjw==}
'@mi-gpt/utils@1.1.1':
resolution: {integrity: sha512-FiGHV2RDBRGE/Xb9Q5H757t3ZqOpo6gL+44VXiRxQb3fQiapOpn038RAWErHJYZajYzkxP2jl8Wggsb+wp77Zg==}
engines: {node: '>=16'}
'@neon-rs/cli@0.1.82':
@@ -671,34 +671,34 @@ snapshots:
'@esbuild/win32-x64@0.25.2':
optional: true
'@mi-gpt/chat@1.0.0':
'@mi-gpt/chat@1.1.1':
dependencies:
'@mi-gpt/openai': 1.0.0
'@mi-gpt/stream': 1.0.0
'@mi-gpt/utils': 1.0.0
'@mi-gpt/openai': 1.1.1
'@mi-gpt/stream': 1.1.1
'@mi-gpt/utils': 1.1.1
transitivePeerDependencies:
- encoding
- supports-color
- ws
- zod
'@mi-gpt/config@1.0.0': {}
'@mi-gpt/config@1.1.0': {}
'@mi-gpt/engine@1.1.1':
'@mi-gpt/engine@1.2.1':
dependencies:
'@mi-gpt/chat': 1.0.0
'@mi-gpt/openai': 1.0.0
'@mi-gpt/stream': 1.0.0
'@mi-gpt/utils': 1.0.0
'@mi-gpt/chat': 1.1.1
'@mi-gpt/openai': 1.1.1
'@mi-gpt/stream': 1.1.1
'@mi-gpt/utils': 1.1.1
transitivePeerDependencies:
- encoding
- supports-color
- ws
- zod
'@mi-gpt/openai@1.0.0':
'@mi-gpt/openai@1.1.1':
dependencies:
'@mi-gpt/utils': 1.0.0
'@mi-gpt/utils': 1.1.1
openai: 4.93.0
proxy-agent: 6.5.0
transitivePeerDependencies:
@@ -707,11 +707,11 @@ snapshots:
- ws
- zod
'@mi-gpt/stream@1.0.0':
'@mi-gpt/stream@1.1.1':
dependencies:
'@mi-gpt/utils': 1.0.0
'@mi-gpt/utils': 1.1.1
'@mi-gpt/utils@1.0.0': {}
'@mi-gpt/utils@1.1.1': {}
'@neon-rs/cli@0.1.82':
optionalDependencies:
@@ -6,6 +6,6 @@
"@/*": ["migpt/*"]
}
},
"include": ["migpt"],
"include": ["migpt", "config.ts"],
"exclude": ["node_modules"]
}
+2
View File
@@ -0,0 +1,2 @@
*.mp3
*.wav
+547
View File
@@ -0,0 +1,547 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "alsa"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812947049edcd670a82cd5c73c3661d2e58468577ba8489de58e1a73c04cbd5d"
dependencies = [
"alsa-sys",
"bitflags",
"cfg-if",
"libc",
]
[[package]]
name = "alsa-sys"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad7569085a265dd3f607ebecce7458eaab2132a84393534c95b18dcbc3f31e04"
dependencies = [
"libc",
"pkg-config",
]
[[package]]
name = "anyhow"
version = "1.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a23eb6b1614318a8071c9b2521f36b424b2c83db5eb3a0fead4a6c0809af6e61"
[[package]]
name = "atomic-polyfill"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8cf2bce30dfe09ef0bfaef228b9d414faaf7e563035494d7fe092dba54b300f4"
dependencies = [
"critical-section",
]
[[package]]
name = "audiopus_sys"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62314a1546a2064e033665d658e88c620a62904be945f8147e6b16c3db9f8651"
dependencies = [
"cmake",
"log",
"pkg-config",
]
[[package]]
name = "bitflags"
version = "2.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3"
[[package]]
name = "byteorder"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]]
name = "bytes"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b35204fbdc0b3f4446b89fc1ac2cf84a8a68971995d0bf2e925ec7cd960f9cb3"
[[package]]
name = "cc"
version = "1.2.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a0aeaff4ff1a90589618835a598e545176939b97874f7abc7851caa0618f203"
dependencies = [
"find-msvc-tools",
"shlex",
]
[[package]]
name = "cfg-if"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
[[package]]
name = "cmake"
version = "0.1.57"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75443c44cd6b379beb8c5b45d85d0773baf31cce901fe7bb252f4eff3008ef7d"
dependencies = [
"cc",
]
[[package]]
name = "cobs"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1"
dependencies = [
"thiserror",
]
[[package]]
name = "critical-section"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "790eea4361631c5e7d22598ecd5723ff611904e3344ce8720784c93e3d83d40b"
[[package]]
name = "embedded-io"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced"
[[package]]
name = "embedded-io"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d"
[[package]]
name = "errno"
version = "0.3.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "find-msvc-tools"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "645cbb3a84e60b7531617d5ae4e57f7e27308f6445f5abf653209ea76dec8dff"
[[package]]
name = "hash32"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b0c35f58762feb77d74ebe43bdbc3210f09be9fe6742234d573bacc26ed92b67"
dependencies = [
"byteorder",
]
[[package]]
name = "heapless"
version = "0.7.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdc6457c0eb62c71aac4bc17216026d8410337c4126773b9c5daba343f17964f"
dependencies = [
"atomic-polyfill",
"hash32",
"rustc_version",
"serde",
"spin",
"stable_deref_trait",
]
[[package]]
name = "libc"
version = "0.2.178"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091"
[[package]]
name = "lock_api"
version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
dependencies = [
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
[[package]]
name = "mio"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a69bcab0ad47271a0234d9422b131806bf3968021e5dc9328caf2d4cd58557fc"
dependencies = [
"libc",
"wasi",
"windows-sys 0.61.2",
]
[[package]]
name = "opus"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6526409b274a7e98e55ff59d96aafd38e6cd34d46b7dbbc32ce126dffcd75e8e"
dependencies = [
"audiopus_sys",
"libc",
]
[[package]]
name = "parking_lot"
version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-link",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pkg-config"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7edddbd0b52d732b21ad9a5fab5c704c14cd949e5e9a1ec5929a24fded1b904c"
[[package]]
name = "postcard"
version = "1.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24"
dependencies = [
"cobs",
"embedded-io 0.4.0",
"embedded-io 0.6.1",
"heapless",
"serde",
]
[[package]]
name = "proc-macro2"
version = "1.0.104"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9695f8df41bb4f3d222c95a67532365f569318332d03d5f3f67f37b20e6ebdf0"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a338cc41d27e6cc6dce6cefc13a0729dfbb81c262b1f519331575dd80ef3067f"
dependencies = [
"proc-macro2",
]
[[package]]
name = "redox_syscall"
version = "0.5.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
dependencies = [
"bitflags",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "semver"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d767eb0aabc880b29956c35734170f26ed551a859dbd361d140cdbeca61ab1e2"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "shlex"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
[[package]]
name = "signal-hook-registry"
version = "1.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
dependencies = [
"errno",
"libc",
]
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "socket2"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17129e116933cf371d018bb80ae557e889637989d8638274fb25622827b03881"
dependencies = [
"libc",
"windows-sys 0.60.2",
]
[[package]]
name = "spin"
version = "0.9.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67"
dependencies = [
"lock_api",
]
[[package]]
name = "stable_deref_trait"
version = "1.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
[[package]]
name = "stereo"
version = "0.1.0"
dependencies = [
"alsa",
"anyhow",
"opus",
"postcard",
"serde",
"tokio",
]
[[package]]
name = "syn"
version = "2.0.111"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "390cc9a294ab71bdb1aa2e99d13be9c753cd2d7bd6560c77118597410c4d2e87"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "thiserror"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f63587ca0f12b72a0600bcba1d40081f830876000bb46dd2337a3051618f4fc8"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff15c8ecd7de3849db632e14d18d2571fa09dfc5ed93479bc4485c7a517c913"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.48.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ff360e02eab121e0bc37a2d3b4d4dc622e6eda3a8e5253d5435ecf5bd4c68408"
dependencies = [
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys 0.61.2",
]
[[package]]
name = "tokio-macros"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "unicode-ident"
version = "1.0.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5"
[[package]]
name = "wasi"
version = "0.11.1+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-targets"
version = "0.53.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3"
dependencies = [
"windows-link",
"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.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53"
[[package]]
name = "windows_aarch64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006"
[[package]]
name = "windows_i686_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3"
[[package]]
name = "windows_i686_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c"
[[package]]
name = "windows_i686_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2"
[[package]]
name = "windows_x86_64_gnu"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1"
[[package]]
name = "windows_x86_64_msvc"
version = "0.53.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650"
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "stereo"
version = "0.1.0"
edition = "2024"
[profile.release]
lto = true
opt-level = "s"
codegen-units = 1
panic = "abort"
strip = true
debug = false
[dependencies]
opus = "0.3"
anyhow = "1.0"
tokio = { version = "1.48", features = ["full"] }
serde = { version = "1.0", features = ["derive"] }
postcard = { version = "1.0", features = ["alloc", "use-std"] }
[target.'cfg(target_os = "linux")'.dependencies]
alsa = "0.11"
+10
View File
@@ -0,0 +1,10 @@
build:
docker run --rm -v $(shell pwd):/app idootop/open-xiaoai-runtime:oh2p \
cargo build --target armv7-unknown-linux-gnueabihf --release
# 部署到小爱音箱(调试自用)
deploy:
dd if=target/armv7-unknown-linux-gnueabihf/release/stereo \
| sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.153 "dd of=/data/stereo"
dd if=target/armv7-unknown-linux-gnueabihf/release/stereo \
| sshpass -p open-xiaoai ssh -o HostKeyAlgorithms=+ssh-rsa root@192.168.31.235 "dd of=/data/stereo"
+59
View File
@@ -0,0 +1,59 @@
# Open-XiaoAI Stereo
不同型号的小爱音箱组立体声。
## 功能
- 在小爱音箱上独立运行,无需服务端部署
- 支持不同型号的小爱音箱局域网组立体声
- 支持蓝牙播放和小爱音箱自带的音乐播放
- 支持局域网内主从设备服务发现自动连接
- 支持主从节点“热拔插”,自动切换声道
- 支持前向纠错(FEC)和丢包补偿(PLC)
## 快速开始
> [!NOTE]
> 以下操作需要先将小爱音箱刷机, 然后 SSH 连接到小爱音箱。👉 [教程](../../docs/flash.md)
首先,分别在两台设备上配置主从和声道。
```shell
# 首先确保配置文件路径存在
mkdir -p /data/open-xiaoai || true
# 如果你想让左边的小爱音箱作为主音源
echo master left > /data/open-xiaoai/stereo.conf # 在左边的小爱音箱上执行
echo slave right > /data/open-xiaoai/stereo.conf # 在右边的小爱音箱上执行
# 如果你想让右边的小爱音箱作为主音源
# echo master right > /data/open-xiaoai/stereo.conf # 在右边的小爱音箱上执行
# echo slave left > /data/open-xiaoai/stereo.conf # 在左边的小爱音箱上执行
```
配置完成后,分别在两台设备上运行下面的启动脚本即可。
```shell
curl -sSfL https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-stereo/init.sh | sh
```
如果你想要开机自启动,运行以下命令重启小爱音箱即可(需要先按上面的步骤配置主从设备和声道)。
```shell
# 下载 boot.sh 到 /data/init.sh 开机时自启动
curl -L -o /data/init.sh https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-stereo/boot.sh
# 重启小爱音箱
reboot
```
## 注意事项
- 不支持同步音量大小(不同型号设备的音量基准不同,请手动调整)
- 不支持多路音频混音(同时播放多个音频听起来像是信号故障、静音卡顿)
- 目前只支持小爱音箱 Pro(LX06)和小米智能音箱 Pro(OH2P)这两个机型(需要刷机)
- 从设备会主动同步主设备的音源,反之不支持(建议禁用从设备的麦克风和操作,防止音频叠加)
## License
MIT License © 2024-PRESENT [Del Wang](https://del.wang)
+68
View File
@@ -0,0 +1,68 @@
#!/bin/sh
exec > /dev/null 2>&1
WORK_DIR="/data/open-xiaoai"
APP_BINARY="$WORK_DIR/stereo"
CONFIG_FILE="$WORK_DIR/stereo.conf"
DOWNLOAD_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-stereo/stereo"
cat << 'EOF'
▄▖ ▖▖▘ ▄▖▄▖
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
▌ - Stereo(立体声)
v1.0.0 by: https://del.wang
EOF
# 等待能够正常访问 baidu.com
while ! ping -c 1 baidu.com > /dev/null 2>&1; do
echo "🤫 等待网络连接中..."
sleep 1
done
sleep 3
echo "✅ 网络连接成功"
main() {
# 1. 确保目录存在
[ -d "$WORK_DIR" ] || mkdir -p "$WORK_DIR"
# 2. 检查并下载程序
if [ ! -f "$APP_BINARY" ]; then
echo "🚀 正在下载 Stereo 立体声程序..."
if ! curl -L -# -f -o "$APP_BINARY" "$DOWNLOAD_URL"; then
echo "❌ 下载失败,请检查网络连接。"
exit 1
fi
chmod +x "$APP_BINARY"
echo "✅ 程序下载完毕"
fi
# 3. 如果运行脚本时带了参数,则覆盖更新配置文件
if [ $# -gt 0 ]; then
echo "$*" > "$CONFIG_FILE"
fi
# 4. 读取配置
local ARGS=""
if [ -f "$CONFIG_FILE" ]; then
ARGS=$(cat "$CONFIG_FILE")
fi
# 5. 停止旧进程
PID=$(pgrep -f "$APP_BINARY" || true)
if [ -n "$PID" ]; then
kill -9 $PID > /dev/null 2>&1 || true
fi
# 6. 启动程序
echo "🔥 Stereo 立体声程序启动中..."
"$APP_BINARY" $ARGS
}
main "$@"
+58
View File
@@ -0,0 +1,58 @@
#!/bin/sh
set -eu
WORK_DIR="/data/open-xiaoai"
APP_BINARY="$WORK_DIR/stereo"
CONFIG_FILE="$WORK_DIR/stereo.conf"
DOWNLOAD_URL="https://gitee.com/idootop/artifacts/releases/download/open-xiaoai-stereo/stereo"
cat << 'EOF'
▄▖ ▖▖▘ ▄▖▄▖
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
▌ - Stereo(立体声)
v1.0.0 by: https://del.wang
EOF
main() {
# 1. 确保目录存在
[ -d "$WORK_DIR" ] || mkdir -p "$WORK_DIR"
# 2. 检查并下载程序
if [ ! -f "$APP_BINARY" ]; then
echo "🚀 正在下载 Stereo 立体声程序..."
if ! curl -L -# -f -o "$APP_BINARY" "$DOWNLOAD_URL"; then
echo "❌ 下载失败,请检查网络连接。"
exit 1
fi
chmod +x "$APP_BINARY"
echo "✅ 程序下载完毕"
fi
# 3. 如果运行脚本时带了参数,则覆盖更新配置文件
if [ $# -gt 0 ]; then
echo "$*" > "$CONFIG_FILE"
fi
# 4. 读取配置
local ARGS=""
if [ -f "$CONFIG_FILE" ]; then
ARGS=$(cat "$CONFIG_FILE")
fi
# 5. 停止旧进程
PID=$(pgrep -f "$APP_BINARY" || true)
if [ -n "$PID" ]; then
kill -9 $PID > /dev/null 2>&1 || true
fi
# 6. 启动程序
echo "🔥 Stereo 立体声程序启动中..."
exec "$APP_BINARY" $ARGS
}
main "$@"
+35
View File
@@ -0,0 +1,35 @@
#![cfg(target_os = "linux")]
use crate::app::master::run_master;
use crate::app::slave::run_slave;
use crate::net::protocol::ChannelRole;
use anyhow::Result;
use std::env;
pub async fn run_stereo() -> Result<()> {
let args: Vec<String> = env::args().collect();
if args.len() < 3 {
eprintln!("用法: {} [master|slave] [left|right]", args[0]);
return Ok(());
}
let mode = if args[1].to_lowercase() == "master" {
"主节点"
} else {
"从节点"
};
let role = if args[2].to_lowercase() == "left" {
ChannelRole::Left
} else {
ChannelRole::Right
};
println!("🚗 当前为: {} {}", mode, role.to_string());
if mode == "主节点" {
run_master(role).await
} else {
run_slave(role).await
}
}
+365
View File
@@ -0,0 +1,365 @@
#![cfg(target_os = "linux")]
use crate::audio::codec::OpusCodec;
use crate::audio::config::AudioConfig;
use crate::audio::player::AudioPlayer;
use crate::net::discovery::Discovery;
use crate::net::network::{ControlConnection, MasterNetwork};
use crate::net::protocol::{AudioPacket, ChannelRole, ControlPacket};
use crate::utils::alsa::AlsaRedirector;
use crate::utils::sync::now_us;
use anyhow::{Result, anyhow};
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::signal::unix::{SignalKind, signal};
use tokio::sync::Mutex;
pub const SERVER_TCP_PORT: u16 = 53531;
#[derive(Clone)]
struct SlaveSession {
udp_addr: SocketAddr,
role: ChannelRole,
}
pub async fn run_master(master_role: ChannelRole) -> Result<()> {
// 0. 设置 ALSA 重定向
println!("🔥 启动中,请稍等...");
let _alsa_guard = AlsaRedirector::new()?;
// 1. 设置网络 (UDP + TCP)
let network = MasterNetwork::setup(SERVER_TCP_PORT).await?;
let audio_socket = network.audio_socket().clone_inner();
// 2. 启动服务发现广播
Discovery::start_broadcast(SERVER_TCP_PORT).await?;
println!("✅ 服务已启动,等待连接...");
let shutdown_flag = Arc::new(AtomicBool::new(false));
let slaves = Arc::new(Mutex::new(Vec::<SlaveSession>::new()));
// 3. 启动连接监听任务
let slaves_clone = slaves.clone();
let audio_socket_clone = audio_socket.clone();
tokio::spawn(async move {
loop {
match network.accept().await {
Ok((control_conn, client_addr)) => {
let slaves_for_session = slaves_clone.clone();
let audio_socket_for_session = audio_socket_clone.clone();
tokio::spawn(async move {
if let Err(e) = handle_master_session(
control_conn,
audio_socket_for_session,
slaves_for_session,
client_addr.to_string(),
)
.await
{
eprintln!("❌ 会话错误: {:?}", e);
}
});
}
Err(e) => {
eprintln!("❌ Accept 错误: {:?}", e);
}
}
}
});
// 4. 音频处理主循环
let config = AudioConfig::music();
let encode_config = AudioConfig {
channels: 1,
vbr: true,
..AudioConfig::music()
};
let player = AudioPlayer::new(&AudioConfig {
channels: 2,
playback_device: "plug:original_default".into(),
..config.clone()
})?;
let mut raw_buf = vec![0u8; config.frame_size * 2 * 2];
let mut pcm_out = vec![0i16; config.frame_size * 2];
let mut left_pcm = vec![0i16; config.frame_size];
let mut right_pcm = vec![0i16; config.frame_size];
let mut opus_out = vec![0u8; 1500];
let mut seq = 0u32;
// 播放延迟: 只需覆盖网络延迟 + 时钟偏移
let delay_us = 100_000; // 100ms 基础延迟
let frame_duration_us =
(config.frame_size as f64 / config.sample_rate as f64 * 1_000_000.0) as u128;
let mut stream_start_ts = 0;
let mut stream_start_seq = 0;
let shutdown_flag_clone = shutdown_flag.clone();
let audio_loop = async move {
loop {
if shutdown_flag_clone.load(Ordering::Relaxed) {
break;
}
// 打开 FIFO
let mut fifo = match tokio::fs::File::open(AlsaRedirector::fifo_path()).await {
Ok(f) => f,
Err(_) => {
if shutdown_flag_clone.load(Ordering::Relaxed) {
break;
}
tokio::time::sleep(Duration::from_secs(1)).await;
continue;
}
};
// 每个新流开始时,重置编码器状态以避免残留音频导致爆音
let mut left_encoder = OpusCodec::new(&encode_config)?;
let mut right_encoder = OpusCodec::new(&encode_config)?;
loop {
if shutdown_flag_clone.load(Ordering::Relaxed) {
break;
}
// 从 FIFO 读取
if let Err(_) = fifo.read_exact(&mut raw_buf).await {
break; // FIFO 关闭,重新打开
}
let active_slaves = {
let s = slaves.lock().await;
if s.is_empty() { None } else { Some(s.clone()) }
};
let now = now_us();
if stream_start_ts == 0 {
stream_start_ts = now;
stream_start_seq = seq;
}
// 计算该帧应当播放的目标时间
// target_ts = 数据包发送时间 + 播放延迟
let target_ts = stream_start_ts
+ ((seq - stream_start_seq) as u128 * frame_duration_us)
+ delay_us;
// 提取 PCM 数据
for i in 0..config.frame_size {
left_pcm[i] = i16::from_le_bytes([raw_buf[i * 4], raw_buf[i * 4 + 1]]);
right_pcm[i] = i16::from_le_bytes([raw_buf[i * 4 + 2], raw_buf[i * 4 + 3]]);
}
if let Some(slaves_list) = active_slaves {
// 情况 1: 有从节点,进行网络传输,并本地构造静音声道回放
// 1. 检查各声道是否有从节点需要
let needs_left = slaves_list.iter().any(|s| s.role == ChannelRole::Left);
let needs_right = slaves_list.iter().any(|s| s.role == ChannelRole::Right);
// 2. 编码需要的声道
let mut left_bytes = None;
let mut right_bytes = None;
if needs_left {
let len = left_encoder.encode(&left_pcm, &mut opus_out)?;
let packet = AudioPacket {
seq,
timestamp: target_ts,
data: opus_out[..len].to_vec(),
};
left_bytes = Some(postcard::to_allocvec(&packet)?);
}
if needs_right {
let len = right_encoder.encode(&right_pcm, &mut opus_out)?;
let packet = AudioPacket {
seq,
timestamp: target_ts,
data: opus_out[..len].to_vec(),
};
right_bytes = Some(postcard::to_allocvec(&packet)?);
}
// 3. 发送给对应的从节点
for slave in &slaves_list {
let bytes = match slave.role {
ChannelRole::Left => left_bytes.as_ref(),
ChannelRole::Right => right_bytes.as_ref(),
};
if let Some(b) = bytes {
let _ = audio_socket.send_to(b, slave.udp_addr).await;
}
}
// 4. 将非本节点的声道置为静音
for i in 0..config.frame_size {
match master_role {
ChannelRole::Left => {
pcm_out[i * 2] = left_pcm[i];
pcm_out[i * 2 + 1] = 0;
}
ChannelRole::Right => {
pcm_out[i * 2] = 0;
pcm_out[i * 2 + 1] = right_pcm[i];
}
}
}
// 5. 等待播放
let now = now_us();
if now < target_ts {
let wait = target_ts - now;
if wait > 1000 {
tokio::time::sleep(Duration::from_micros(wait as u64)).await;
} else {
// 小于 1ms,直接播放,让播放时机稍微早一点点
}
}
} else {
// 情况 2: 没有从节点,本地立体声播放
for i in 0..config.frame_size {
pcm_out[i * 2] = left_pcm[i];
pcm_out[i * 2 + 1] = right_pcm[i];
}
}
// 统一写入播放器 (始终是立体声)
if let Err(_) = player.write(&pcm_out) {
if shutdown_flag_clone.load(Ordering::Relaxed) {
break;
}
}
seq += 1;
}
// 重置流计时
stream_start_ts = 0;
}
Ok::<(), anyhow::Error>(())
};
tokio::select! {
res = audio_loop => {
if let Err(e) = res {
eprintln!("❌ 音频循环错误: {:?}", e);
}
},
_ = shutdown_signal() => {
// 设置退出标志,通知音频循环停止
shutdown_flag.store(true, Ordering::Relaxed);
},
}
// 显式清理
println!("👋 正在退出...");
AlsaRedirector::cleanup();
// 强制退出
std::process::exit(0);
}
/// 监听系统退出信号 (SIGINT, SIGTERM, SIGQUIT)
async fn shutdown_signal() {
let mut sigint = signal(SignalKind::interrupt()).expect("无法注册 SIGINT 处理器");
let mut sigterm = signal(SignalKind::terminate()).expect("无法注册 SIGTERM 处理器");
let mut sigquit = signal(SignalKind::quit()).expect("无法注册 SIGQUIT 处理器");
tokio::select! {
_ = sigint.recv() => {},
_ = sigterm.recv() => {},
_ = sigquit.recv() => {},
}
}
/// 处理主节点与从节点的会话
async fn handle_master_session(
mut control: ControlConnection,
audio_socket: Arc<tokio::net::UdpSocket>,
slaves: Arc<Mutex<Vec<SlaveSession>>>,
client_tcp_addr: String,
) -> Result<()> {
let mut buf = [0u8; 1024];
// 握手
let pkt = control.recv_packet(&mut buf).await?;
let slave_role = match pkt {
ControlPacket::ClientIdentify { role } => role,
_ => return Err(anyhow!("无效的握手协议")),
};
let stereo = ControlPacket::ServerHello {
udp_port: audio_socket.local_addr()?.port(),
};
control.send_packet(&stereo).await?;
// 等待 UDP 打洞/确认
let mut buf = [0u8; 128];
let (_, client_udp_addr) = audio_socket.recv_from(&mut buf).await?;
println!(
"✅ 从节点已连接: {} {}",
client_tcp_addr,
slave_role.to_string(),
);
// 添加到从节点列表
let session = SlaveSession {
udp_addr: client_udp_addr,
role: slave_role,
};
{
let mut s = slaves.lock().await;
s.push(session.clone());
}
// 分离 TCP 读写,处理控制消息和心跳
let (mut tcp_rx, mut tcp_tx) = control.split();
let mut buf = [0u8; 1024];
loop {
match tcp_rx.read(&mut buf).await {
Ok(0) | Err(_) => {
break;
}
Ok(n) => {
if let Ok(ControlPacket::Ping { client_ts, seq }) = postcard::from_bytes(&buf[..n])
{
let pong = ControlPacket::Pong {
client_ts,
server_ts: now_us(),
seq,
};
if tcp_tx
.write_all(&postcard::to_allocvec(&pong).unwrap())
.await
.is_err()
{
break;
}
}
}
}
}
println!(
"❌ 从节点已断开: {} {}",
client_tcp_addr,
slave_role.to_string(),
);
// 从列表中移除
{
let mut s = slaves.lock().await;
s.retain(|x| x.udp_addr != client_udp_addr);
}
Ok(())
}
+3
View File
@@ -0,0 +1,3 @@
pub mod entry;
pub mod master;
pub mod slave;
+199
View File
@@ -0,0 +1,199 @@
#![cfg(target_os = "linux")]
use crate::audio::codec::OpusCodec;
use crate::audio::config::AudioConfig;
use crate::audio::player::AudioPlayer;
use crate::net::discovery::Discovery;
use crate::net::network::SlaveNetwork;
use crate::net::protocol::{AudioPacket, ChannelRole, ControlPacket};
use crate::utils::sync::{ClockSync, now_us};
use anyhow::{Result, anyhow};
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::sync::{Mutex, mpsc};
/// 运行从节点模式
pub async fn run_slave(role: ChannelRole) -> Result<()> {
loop {
match handle_connection(role.clone()).await {
Err(e) => {
eprintln!("{:?}", e);
tokio::time::sleep(Duration::from_secs(3)).await;
}
Ok(_) => {}
}
}
}
async fn handle_connection(role: ChannelRole) -> Result<()> {
// 1. 发现主节点
println!("🔍 正在扫描主节点...");
let (master_ip, master_tcp_port) = Discovery::discover_master().await?;
let master_tcp_addr = format!("{}:{}", master_ip, master_tcp_port);
// 2. 建立 TCP 连接
println!("🔥 发现主节点: {}", master_tcp_addr);
let network = SlaveNetwork::connect(master_tcp_addr.parse()?).await?;
let (mut control, audio) = network.split();
// 3. 身份认证
control
.send_packet(&ControlPacket::ClientIdentify { role: role.clone() })
.await?;
let mut buf = [0u8; 1024];
let pkt = control.recv_packet(&mut buf).await?;
let server_udp_port = match pkt {
ControlPacket::ServerHello { udp_port } => udp_port,
_ => return Err(anyhow!("身份认证应答异常")),
};
// 4. UDP 打洞
audio
.punch(format!("{}:{}", master_ip, server_udp_port).parse()?)
.await?;
// 5. 初始化音频与同步组件
let config = AudioConfig {
channels: 1,
..AudioConfig::music()
};
let player = AudioPlayer::new(&config)?;
let mut codec = OpusCodec::new(&config)?;
let clock = Arc::new(Mutex::new(ClockSync::new(100)));
// 用于通知主循环 TCP 已断开的消息通道
let (disconnect_tx, mut disconnect_rx) = mpsc::channel::<()>(1);
// 6. 分离 TCP 读写
let (mut tcp_rx, mut tcp_tx) = control.split();
let clock_updater = clock.clone();
let d_tx_ping = disconnect_tx.clone();
let d_tx_pong = disconnect_tx.clone();
// 定时发送 Ping (心跳 & 时间同步)
let _sync_handle = tokio::spawn(async move {
let mut seq = 0;
loop {
let t1 = now_us();
let msg = ControlPacket::Ping { client_ts: t1, seq };
let data = postcard::to_allocvec(&msg).unwrap();
if tcp_tx.write_all(&data).await.is_err() {
let _ = d_tx_ping.send(()).await; // 通知主线程 TCP 失败
break;
}
tokio::time::sleep(Duration::from_millis(200)).await;
seq += 1;
}
});
// 接收 Pong
tokio::spawn(async move {
let mut buf = [0u8; 1024];
loop {
match tcp_rx.read(&mut buf).await {
Ok(n) if n > 0 => {
if let Ok(ControlPacket::Pong {
client_ts,
server_ts,
..
}) = postcard::from_bytes(&buf[..n])
{
let t4 = now_us();
clock_updater.lock().await.update(client_ts, server_ts, t4);
}
}
_ => {
let _ = d_tx_pong.send(()).await; // TCP 断开
break;
}
}
}
});
// 7. 接收音频数据包 (UDP)
let (audio_tx, mut audio_rx) = mpsc::channel(100);
let audio_socket = audio.clone_inner();
tokio::spawn(async move {
let mut buf = [0u8; 2048];
loop {
if let Ok((len, _)) = audio_socket.recv_from(&mut buf).await {
if let Ok(packet) = postcard::from_bytes::<AudioPacket>(&buf[..len]) {
if audio_tx.send(packet).await.is_err() {
break;
}
}
}
}
});
// 8. 播放提示
println!("✅ 主节点已连接,音频串流中...");
let role_str = role.to_string();
tokio::spawn(async move {
let _ = tokio::process::Command::new("sh")
.arg("-c")
.arg(format!(
"/usr/sbin/tts_play.sh \"主节点已连接,{}\" >/dev/null 2>&1",
role_str
))
.status()
.await;
});
// 9. 播放主循环
let mut pcm_buf = vec![0i16; config.frame_size];
let mut last_seq: Option<u32> = None;
loop {
// 检查 TCP 是否已断开
if let Ok(_) = disconnect_rx.try_recv() {
return Err(anyhow!("主节点已断开: {}", master_tcp_addr));
}
// 接收数据包
if let Ok(pkt) = audio_rx.try_recv() {
let now = now_us();
let current_server_time = clock.lock().await.to_server_time(now);
// 检查包是否迟到(目标时间已过)
if current_server_time > pkt.timestamp {
let late_ms = (current_server_time - pkt.timestamp) / 1000;
if late_ms > 50 {
// 迟到超过 50ms,直接丢弃
continue;
}
// 轻微迟到(<50ms),尝试播放
}
last_seq = Some(pkt.seq);
// 精确等待到播放时间
loop {
let now = now_us();
let current_server_time_precise = clock.lock().await.to_server_time(now);
if current_server_time_precise >= pkt.timestamp {
break;
}
let wait_us = (pkt.timestamp - current_server_time_precise) as u64;
if wait_us > 1000 {
tokio::time::sleep(Duration::from_micros(wait_us as u64)).await;
} else {
// 小于 1ms,直接播放,让播放时机稍微早一点点
break;
}
}
// 解码并播放
if let Ok(len) = codec.decode(&pkt.data, &mut pcm_buf) {
let _ = player.write(&pcm_buf[..len]);
}
} else {
tokio::time::sleep(Duration::from_micros(100)).await;
}
}
}
+70
View File
@@ -0,0 +1,70 @@
use crate::audio::config::{AudioConfig, AudioScene};
use anyhow::{Context, Result};
use opus::{Application, Bitrate, Channels, Decoder, Encoder};
pub struct OpusCodec {
encoder: Encoder,
decoder: Decoder,
}
impl OpusCodec {
pub fn new(config: &AudioConfig) -> Result<Self> {
let channels = match config.channels {
1 => Channels::Mono,
2 => Channels::Stereo,
_ => return Err(anyhow::anyhow!("Invalid channels: {}", config.channels)),
};
let mode = match config.audio_scene {
AudioScene::Music => Application::Audio,
AudioScene::Voice => Application::Voip,
};
let bitrate = match config.bitrate {
-1 => Bitrate::Max,
0 => Bitrate::Auto,
_ => Bitrate::Bits(config.bitrate),
};
let mut encoder = Encoder::new(config.sample_rate, channels, mode)
.context("Failed to create Opus encoder")?;
encoder.set_bitrate(bitrate)?;
if config.vbr {
encoder.set_vbr(true)?;
}
if config.fec {
encoder.set_inband_fec(true)?; // 内联前向纠错
encoder.set_packet_loss_perc(20)?; // 预期丢包率20%
}
let decoder =
Decoder::new(config.sample_rate, channels).context("Failed to create Opus decoder")?;
Ok(Self { encoder, decoder })
}
pub fn encode(&mut self, pcm: &[i16], out: &mut [u8]) -> Result<usize> {
self.encoder
.encode(pcm, out)
.context("Opus encoding failed")
}
pub fn decode(&mut self, opus: &[u8], out: &mut [i16]) -> Result<usize> {
self.decoder
.decode(opus, out, false)
.context("Opus decoding failed")
}
/// 前向纠错(FEC)
pub fn decode_fec(&mut self, opus: &[u8], out: &mut [i16]) -> Result<usize> {
self.decoder
.decode(opus, out, true)
.context("Opus FEC decoding failed")
}
/// 丢包补偿(PLC)
pub fn decode_loss(&mut self, out: &mut [i16]) -> Result<usize> {
self.decoder
.decode(&[], out, false)
.context("Opus PLC (decode_loss) failed")
}
}
+61
View File
@@ -0,0 +1,61 @@
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum AudioScene {
Music,
Voice,
}
#[derive(Debug, Clone)]
pub struct AudioConfig {
// ALSA 设备参数,用于录音和播放
pub capture_device: String,
pub playback_device: String,
pub sample_rate: u32,
pub channels: u16,
pub frame_size: usize, // 帧大小,单位为采样点
// Opus 编解码参数,用于音频传输
pub audio_scene: AudioScene,
pub bitrate: i32,
pub vbr: bool, // 是否启用 VBR(动态比特率)
pub fec: bool, // 是否启用 FEC(内联前向纠错)
}
impl AudioConfig {
pub fn music() -> Self {
Self {
audio_scene: AudioScene::Music,
sample_rate: 48_000, // 48kHz
channels: 2,
frame_size: 960, // 20ms at 48kHz
bitrate: 320_000, // 320 kbps
..Default::default()
}
}
pub fn voice() -> Self {
Self {
audio_scene: AudioScene::Voice,
sample_rate: 16_000, // 16kHz
channels: 1,
frame_size: 320, // 20ms at 16kHz
bitrate: 32_000, // 32 kbps
..Default::default()
}
}
}
impl Default for AudioConfig {
fn default() -> Self {
Self {
audio_scene: AudioScene::Voice,
capture_device: "plug:Capture".to_string(),
playback_device: "default".to_string(),
sample_rate: 16_000,
channels: 1,
frame_size: 320, // 20ms at 16kHz
bitrate: 32_000,
vbr: false,
fec: false,
}
}
}
+4
View File
@@ -0,0 +1,4 @@
pub mod codec;
pub mod config;
pub mod player;
pub mod recorder;
+71
View File
@@ -0,0 +1,71 @@
#![cfg(target_os = "linux")]
use crate::audio::config::AudioConfig;
use alsa::Direction;
use alsa::pcm::{Access, Format, HwParams, PCM};
use anyhow::{Context, Result};
pub struct AudioPlayer {
pcm: PCM,
}
impl AudioPlayer {
pub fn new(config: &AudioConfig) -> Result<Self> {
let pcm = PCM::new(&config.playback_device, Direction::Playback, false)
.context("Failed to open playback PCM device")?;
setup_pcm(&pcm, config.sample_rate, config.channels)?;
Ok(Self { pcm })
}
pub fn write(&self, buffer: &[i16]) -> Result<usize> {
let res = self.pcm.io_i16()?.writei(buffer);
match res {
Ok(written) => Ok(written),
Err(e) => {
// Buffer Underrun,即播放缓冲区的数据被耗尽,导致音频流中断
if e.errno() == 32 {
// 恢复音频流状态
self.pcm.prepare()?;
// 重新获取 IO 对象并尝试写入数据
self.pcm
.io_i16()?
.writei(buffer)
.context("Failed to write to playback device after recovery")
} else {
Err(e).context("Failed to write to playback device")
}
}
}
}
pub fn prepare(&self) -> Result<()> {
self.pcm.prepare().context("Failed to prepare PCM")
}
}
fn setup_pcm(pcm: &PCM, sample_rate: u32, channels: u16) -> Result<()> {
let hwp = HwParams::any(pcm).context("Failed to get HwParams")?;
hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?;
hwp.set_rate(sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels(channels as u32)?;
// 设置较大的缓冲区以减少由于调度抖动和设备重初始化导致的断音/卡顿
// 使用 100ms 缓冲区,既能防止 underrun,又不会引入过大延迟
let buffer_size = (sample_rate as f64 * 0.1) as u32; // 100ms 缓冲
let period_size = buffer_size / 4; // 25ms 周期
hwp.set_buffer_size_near(buffer_size as alsa::pcm::Frames)?;
hwp.set_period_size_near(period_size as alsa::pcm::Frames, alsa::ValueOr::Nearest)?;
pcm.hw_params(&hwp).context("Failed to set HwParams")?;
let swp = pcm.sw_params_current()?;
// 设置 start_threshold,当缓冲区有 1 个 period 数据时就开始播放
// 这样可以快速启动,同时保持足够的缓冲余量
swp.set_start_threshold(period_size as alsa::pcm::Frames)?;
pcm.sw_params(&swp)?;
pcm.prepare()?;
Ok(())
}
+41
View File
@@ -0,0 +1,41 @@
#![cfg(target_os = "linux")]
use crate::audio::config::AudioConfig;
use alsa::Direction;
use alsa::pcm::{Access, Format, HwParams, PCM};
use anyhow::{Context, Result};
pub struct AudioRecorder {
pcm: PCM,
}
impl AudioRecorder {
pub fn new(config: &AudioConfig) -> Result<Self> {
let pcm = PCM::new(&config.capture_device, Direction::Capture, false)
.context("Failed to open capture PCM device")?;
setup_pcm(&pcm, config.sample_rate, config.channels)?;
Ok(Self { pcm })
}
pub fn read(&self, buffer: &mut [i16]) -> Result<usize> {
self.pcm
.io_i16()?
.readi(buffer)
.context("Failed to read from capture device")
}
}
fn setup_pcm(pcm: &PCM, sample_rate: u32, channels: u16) -> Result<()> {
let hwp = HwParams::any(pcm).context("Failed to get HwParams")?;
hwp.set_access(Access::RWInterleaved)?;
hwp.set_format(Format::s16())?;
hwp.set_rate(sample_rate, alsa::ValueOr::Nearest)?;
hwp.set_channels(channels as u32)?;
pcm.hw_params(&hwp).context("Failed to set HwParams")?;
let swp = pcm.sw_params_current()?;
pcm.sw_params(&swp)?;
pcm.prepare()?;
Ok(())
}
+16
View File
@@ -0,0 +1,16 @@
mod app;
mod audio;
mod net;
mod utils;
use anyhow::Result;
#[tokio::main]
async fn main() -> Result<()> {
#[cfg(target_os = "linux")]
{
crate::app::entry::run_stereo().await.unwrap();
}
println!("Only support Linux");
Ok(())
}
+50
View File
@@ -0,0 +1,50 @@
use crate::net::protocol::ControlPacket;
use anyhow::Result;
use std::net::{IpAddr, SocketAddr};
use std::time::Duration;
use tokio::net::UdpSocket;
pub const DISCOVERY_PORT: u16 = 53530;
const DISCOVERY_MAGIC: &[u8] = b"STEREO_DISCOVERY_V1";
/// 服务发现模块,用于主从节点的自动发现
pub struct Discovery;
impl Discovery {
/// 主节点:启动广播,告知从节点自己的 TCP 端口
pub async fn start_broadcast(tcp_port: u16) -> Result<()> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
socket.set_broadcast(true)?;
let target: SocketAddr = format!("255.255.255.255:{}", DISCOVERY_PORT).parse()?;
let mut msg = DISCOVERY_MAGIC.to_vec();
msg.extend(postcard::to_allocvec(&ControlPacket::ServerHello { udp_port: tcp_port })?);
tokio::spawn(async move {
loop {
let _ = socket.send_to(&msg, target).await;
tokio::time::sleep(Duration::from_secs(1)).await;
}
});
Ok(())
}
/// 从节点:监听广播,发现主节点的 IP 和 TCP 端口
pub async fn discover_master() -> Result<(IpAddr, u16)> {
let socket = UdpSocket::bind(format!("0.0.0.0:{}", DISCOVERY_PORT)).await?;
let mut buf = [0u8; 1024];
loop {
let (len, addr) = socket.recv_from(&mut buf).await?;
let data = &buf[..len];
if data.starts_with(DISCOVERY_MAGIC) {
let packet_data = &data[DISCOVERY_MAGIC.len()..];
if let Ok(ControlPacket::ServerHello { udp_port }) = postcard::from_bytes(packet_data) {
return Ok((addr.ip(), udp_port));
}
}
}
}
}
+3
View File
@@ -0,0 +1,3 @@
pub mod discovery;
pub mod network;
pub mod protocol;
+126
View File
@@ -0,0 +1,126 @@
use crate::net::protocol::{AudioPacket, ControlPacket};
use anyhow::{Context, Result};
use std::net::SocketAddr;
use std::sync::Arc;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::{TcpListener, TcpStream, UdpSocket};
/// UDP 音频传输
pub struct AudioSocket {
socket: Arc<UdpSocket>,
}
impl AudioSocket {
pub async fn bind() -> Result<Self> {
let socket = UdpSocket::bind("0.0.0.0:0").await?;
Ok(Self {
socket: Arc::new(socket),
})
}
pub fn local_port(&self) -> Result<u16> {
Ok(self.socket.local_addr()?.port())
}
pub async fn send_packet(&self, packet: &AudioPacket, target: SocketAddr) -> Result<()> {
let bytes = postcard::to_allocvec(packet)?;
self.socket.send_to(&bytes, target).await?;
Ok(())
}
pub async fn recv_packet(&self, buf: &mut [u8]) -> Result<(AudioPacket, SocketAddr)> {
let (len, addr) = self.socket.recv_from(buf).await?;
let packet = postcard::from_bytes(&buf[..len])?;
Ok((packet, addr))
}
pub async fn punch(&self, target: SocketAddr) -> Result<()> {
self.socket.send_to(&[0u8; 1], target).await?;
Ok(())
}
pub fn clone_inner(&self) -> Arc<UdpSocket> {
self.socket.clone()
}
}
/// TCP 控制连接
pub struct ControlConnection {
stream: TcpStream,
}
impl ControlConnection {
pub fn new(stream: TcpStream) -> Self {
Self { stream }
}
pub async fn send_packet(&mut self, packet: &ControlPacket) -> Result<()> {
let bytes = postcard::to_allocvec(packet)?;
self.stream.write_all(&bytes).await?;
Ok(())
}
pub async fn recv_packet(&mut self, buf: &mut [u8]) -> Result<ControlPacket> {
let len = self.stream.read(buf).await?;
if len == 0 {
return Err(anyhow::anyhow!("连接已关闭"));
}
let packet = postcard::from_bytes(&buf[..len])?;
Ok(packet)
}
pub fn split(
self,
) -> (
tokio::net::tcp::OwnedReadHalf,
tokio::net::tcp::OwnedWriteHalf,
) {
self.stream.into_split()
}
}
/// 主节点网络管理器
pub struct MasterNetwork {
listener: TcpListener,
audio: AudioSocket,
}
impl MasterNetwork {
pub async fn setup(port: u16) -> Result<Self> {
let listener = TcpListener::bind(format!("0.0.0.0:{}", port)).await?;
let audio = AudioSocket::bind().await?;
Ok(Self { listener, audio })
}
pub async fn accept(&self) -> Result<(ControlConnection, SocketAddr)> {
let (stream, addr) = self.listener.accept().await?;
Ok((ControlConnection::new(stream), addr))
}
pub fn audio_socket(&self) -> &AudioSocket {
&self.audio
}
}
/// 从节点网络管理器
pub struct SlaveNetwork {
control: ControlConnection,
audio: AudioSocket,
}
impl SlaveNetwork {
pub async fn connect(master_addr: SocketAddr) -> Result<Self> {
let stream = TcpStream::connect(master_addr)
.await
.context(format!("无法连接到主节点 TCP 地址: {}", master_addr))?;
let audio = AudioSocket::bind().await?;
Ok(Self {
control: ControlConnection::new(stream),
audio,
})
}
pub fn split(self) -> (ControlConnection, AudioSocket) {
(self.control, self.audio)
}
}
+45
View File
@@ -0,0 +1,45 @@
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Copy)]
pub enum ChannelRole {
Left,
Right,
}
impl ChannelRole {
pub fn to_string(&self) -> String {
match self {
ChannelRole::Left => "左声道".to_string(),
ChannelRole::Right => "右声道".to_string(),
}
}
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub enum ControlPacket {
// 发现协议
ServerHello {
udp_port: u16, // UDP 音频流端口
},
// 握手协议
ClientIdentify {
role: ChannelRole,
},
// 时间同步 (持续进行)
Ping {
client_ts: u128,
seq: u32,
},
Pong {
client_ts: u128,
server_ts: u128,
seq: u32,
},
}
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct AudioPacket {
pub seq: u32, // 序列号,用于丢包检测
pub timestamp: u128, // 目标播放时间 (主节点时间)
pub data: Vec<u8>, // Opus 编码数据
}
+85
View File
@@ -0,0 +1,85 @@
#![cfg(target_os = "linux")]
use anyhow::{Context, Result};
use std::fs;
use std::process::Command;
const FIFO_PATH: &str = "/tmp/stereo_out.fifo";
const REAL_ASOUND_CONF: &str = "/etc/asound.conf";
const TEMP_ASOUND_CONF: &str = "/tmp/asound.stereo.conf";
/// ALSA 音频重定向器,用于拦截系统音频输出到 FIFO 管道
pub struct AlsaRedirector;
impl AlsaRedirector {
pub fn new() -> Result<Self> {
Self::cleanup(); // 确保环境干净
let original_conf = fs::read_to_string(REAL_ASOUND_CONF).unwrap_or_default();
if !original_conf.contains("pcm.original_default") {
// 重命名原有的 default 逻辑,插入拦截器
let mut new_conf = original_conf.replace("pcm.!default", "pcm.original_default");
new_conf.push_str(&format!(
"\npcm.!default {{ type plug slave {{ pcm \"stereo_interceptor\" format S16_LE rate 48000 channels 2 }} }}\n\
pcm.stereo_interceptor {{ type file slave.pcm \"null\" file \"{}\" format \"raw\" }}\n",
FIFO_PATH
));
fs::write(TEMP_ASOUND_CONF, new_conf)?;
// 挂载覆盖 /etc/asound.conf
let status = Command::new("mount")
.arg("--bind")
.arg(TEMP_ASOUND_CONF)
.arg(REAL_ASOUND_CONF)
.status()
.context("执行 mount 命令失败")?;
if !status.success() {
return Err(anyhow::anyhow!("挂载 asound.conf 失败"));
}
Self::restart_applications();
}
// 创建 FIFO 管道
let _ = Command::new("mkfifo").arg(FIFO_PATH).status();
let _ = Command::new("chmod").arg("666").arg(FIFO_PATH).status();
Ok(Self)
}
pub fn cleanup() {
let _ = Command::new("sh")
.arg("-c")
.arg(format!("umount -l {} >/dev/null 2>&1", REAL_ASOUND_CONF))
.status();
let _ = fs::remove_file(TEMP_ASOUND_CONF);
let _ = fs::remove_file(FIFO_PATH);
Self::restart_applications();
}
pub fn fifo_path() -> &'static str {
FIFO_PATH
}
pub fn restart_applications() {
// 重启媒体播放器
let _ = Command::new("sh")
.arg("-c")
.arg("/etc/init.d/mediaplayer restart >/dev/null 2>&1")
.status();
// 重启蓝牙
let _ = Command::new("sh")
.arg("-c")
.arg("/etc/init.d/bluetooth restart >/dev/null 2>&1")
.status();
}
}
impl Drop for AlsaRedirector {
fn drop(&mut self) {
Self::cleanup();
}
}
+2
View File
@@ -0,0 +1,2 @@
pub mod alsa;
pub mod sync;
+224
View File
@@ -0,0 +1,224 @@
use std::collections::VecDeque;
use std::time::{SystemTime, UNIX_EPOCH};
/// 获取当前微秒级时间戳
pub fn now_us() -> u128 {
SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("时间倒流")
.as_micros()
}
/// 时钟同步管理器,用于计算主从节点间的时钟偏移
/// 采用改进的 NTP 算法 + Kalman 滤波思想
pub struct ClockSync {
/// 偏移量样本窗口
offsets: VecDeque<OffsetSample>,
/// 当前估计的时钟偏移 (server_time - client_time)
pub current_offset: i128,
/// RTT 样本窗口
rtts: VecDeque<i128>,
/// 当前估计的最小 RTT
min_rtt: i128,
/// 窗口大小
window_size: usize,
/// 时钟漂移率 (ppm: parts per million)
/// 正值表示从节点时钟比主节点快
drift_rate: f64,
/// 上次更新时间
last_update_time: u128,
/// 漂移率估计窗口
drift_samples: VecDeque<DriftSample>,
}
#[derive(Clone, Copy)]
struct OffsetSample {
offset: i128,
rtt: i128,
timestamp: u128,
}
#[derive(Clone, Copy)]
struct DriftSample {
offset: i128,
timestamp: u128,
}
impl ClockSync {
pub fn new(window_size: usize) -> Self {
Self {
offsets: VecDeque::with_capacity(window_size),
current_offset: 0,
rtts: VecDeque::with_capacity(window_size),
min_rtt: i128::MAX,
window_size,
drift_rate: 0.0,
last_update_time: now_us(),
drift_samples: VecDeque::with_capacity(60), // 保留 60 秒的样本
}
}
/// 更新时钟偏移估计 (NTP 算法)
///
/// NTP 时间戳标记:
/// t1 = client_send_ts : 客户端发送 Ping 的时间
/// t2 = server_ts : 服务器接收 Ping 的时间
/// t3 = server_ts : 服务器发送 Pong 的时间 (假设处理时间忽略不计)
/// t4 = client_recv_ts : 客户端接收 Pong 的时间
///
/// RTT = (t4 - t1) - (t3 - t2) = (t4 - t1) (因为 t3 = t2)
/// Offset = ((t2 - t1) + (t3 - t4)) / 2 = ((t2 - t1) + (t2 - t4)) / 2
/// = t2 - (t1 + t4) / 2
pub fn update(&mut self, client_send_ts: u128, server_ts: u128, client_recv_ts: u128) {
let t1 = client_send_ts as i128;
let t2 = server_ts as i128;
let t4 = client_recv_ts as i128;
let rtt = t4 - t1;
// 过滤异常 RTT (局域网内 > 100ms 视为异常)
if rtt < 0 || rtt > 100_000 {
return;
}
// 计算时钟偏移: offset = server_time - client_time
// offset = t2 - (t1 + t4) / 2
let offset = t2 - (t1 + t4) / 2;
// 更新 RTT 窗口
self.rtts.push_back(rtt);
if self.rtts.len() > self.window_size {
self.rtts.pop_front();
}
self.min_rtt = *self.rtts.iter().min().unwrap_or(&rtt);
// 更新偏移量窗口
let sample = OffsetSample {
offset,
rtt,
timestamp: client_recv_ts,
};
self.offsets.push_back(sample);
if self.offsets.len() > self.window_size {
self.offsets.pop_front();
}
// 偏移量估计: 使用低 RTT 样本的中位数
// 原理: RTT 较小的样本受网络抖动影响小,时间测量更准确
let mut low_rtt_offsets: Vec<i128> = self
.offsets
.iter()
.filter(|s| s.rtt <= self.min_rtt + 5000) // 5ms 容差
.map(|s| s.offset)
.collect();
if !low_rtt_offsets.is_empty() {
low_rtt_offsets.sort_unstable();
let new_offset = low_rtt_offsets[low_rtt_offsets.len() / 2];
// 漂移率估计
self.estimate_drift(new_offset, client_recv_ts);
// 平滑更新偏移量 (避免突变)
let alpha = 0.3; // 低通滤波系数
self.current_offset =
(alpha * new_offset as f64 + (1.0 - alpha) * self.current_offset as f64) as i128;
}
self.last_update_time = client_recv_ts;
}
/// 估计时钟漂移率
/// 时钟漂移率 = d(offset) / dt
fn estimate_drift(&mut self, offset: i128, timestamp: u128) {
self.drift_samples.push_back(DriftSample { offset, timestamp });
if self.drift_samples.len() > 60 {
self.drift_samples.pop_front();
}
// 至少需要 10 秒的数据才能估计漂移
if self.drift_samples.len() < 10 {
return;
}
// 使用线性回归估计漂移率
let first = self.drift_samples.front().unwrap();
let last = self.drift_samples.back().unwrap();
let dt = (last.timestamp - first.timestamp) as f64;
let d_offset = (last.offset - first.offset) as f64;
if dt > 10_000_000.0 {
// 超过 10 秒
// drift_rate 单位: 微秒/秒 = ppm
let new_drift = d_offset / (dt / 1_000_000.0);
// 平滑更新漂移率
let beta = 0.1;
self.drift_rate = beta * new_drift + (1.0 - beta) * self.drift_rate;
}
}
/// 将本地时间转换为服务器(主节点)时间
/// 考虑时钟漂移补偿
pub fn to_server_time(&self, client_time: u128) -> u128 {
let base_server_time = (client_time as i128 + self.current_offset) as u128;
// 漂移补偿: 根据距离上次同步的时间,补偿时钟漂移
let elapsed_since_update = client_time.saturating_sub(self.last_update_time) as f64;
let drift_correction = (self.drift_rate * elapsed_since_update / 1_000_000.0) as i128;
(base_server_time as i128 + drift_correction) as u128
}
/// 将服务器(主节点)时间转换为本地时间
pub fn to_client_time(&self, server_time: u128) -> u128 {
// 简化版本,不考虑漂移补偿 (播放时主要用 to_server_time)
(server_time as i128 - self.current_offset) as u128
}
/// 获取当前估计的 RTT (微秒)
pub fn get_rtt(&self) -> i128 {
self.min_rtt
}
/// 获取当前时钟漂移率 (ppm)
pub fn get_drift_rate(&self) -> f64 {
self.drift_rate
}
/// 获取同步质量评估 (0-100, 越高越好)
pub fn get_sync_quality(&self) -> u8 {
if self.offsets.is_empty() {
return 0;
}
// 基于 RTT 稳定性和偏移量方差评估
let rtt_variance = self.calculate_variance(&self.rtts.iter().copied().collect::<Vec<_>>());
let offset_variance = self.calculate_variance(
&self.offsets.iter().map(|s| s.offset).collect::<Vec<_>>(),
);
// RTT 越稳定,方差越小,质量越高
let rtt_score = ((100_000.0 - rtt_variance.min(100_000.0)) / 100_000.0 * 50.0) as u8;
let offset_score = ((50_000.0 - offset_variance.min(50_000.0)) / 50_000.0 * 50.0) as u8;
rtt_score + offset_score
}
fn calculate_variance(&self, samples: &[i128]) -> f64 {
if samples.is_empty() {
return 0.0;
}
let mean = samples.iter().sum::<i128>() as f64 / samples.len() as f64;
let variance = samples
.iter()
.map(|&x| {
let diff = x as f64 - mean;
diff * diff
})
.sum::<f64>()
/ samples.len() as f64;
variance.sqrt()
}
}
+1
View File
@@ -0,0 +1 @@
3.12
+25
View File
@@ -0,0 +1,25 @@
{
"version": "0.2.0",
"configurations": [
{
"name": "Python: App",
"type": "debugpy",
"request": "launch",
"program": "${workspaceFolder}/main.py",
"justMyCode": false,
"console": "internalConsole",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env"
},
{
"name": "Python: Current",
"type": "debugpy",
"request": "launch",
"program": "${file}",
"justMyCode": false,
"console": "internalConsole",
"internalConsoleOptions": "openOnSessionStart",
"envFile": "${workspaceFolder}/.env"
}
]
}
+22
View File
@@ -0,0 +1,22 @@
{
"editor.tabSize": 2,
"editor.insertSpaces": true,
"editor.formatOnSave": true,
"python.languageServer": "Pylance",
"python.analysis.autoImportCompletions": true,
"ruff.organizeImports": true,
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.codeActionsOnSave": {
"source.organizeImports": "explicit",
"source.fixAll": "explicit"
}
},
"files.exclude": {
"**/.git": true,
"**/node_modules": true,
"**/__pycache__": true,
"*.egg-info": true,
".mypy_cache": true
}
}
+903
View File
@@ -0,0 +1,903 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "addr2line"
version = "0.24.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dfbe277e56a376000877090da837660b4427aad530e3028d44e0bffe4f89a1c1"
dependencies = [
"gimli",
]
[[package]]
name = "adler2"
version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
[[package]]
name = "autocfg"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26"
[[package]]
name = "backtrace"
version = "0.3.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8d82cb332cdfaed17ae235a638438ac4d4839913cc2af585c3c6746e8f8bee1a"
dependencies = [
"addr2line",
"cfg-if",
"libc",
"miniz_oxide",
"object",
"rustc-demangle",
"windows-targets",
]
[[package]]
name = "bitflags"
version = "2.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd"
[[package]]
name = "block-buffer"
version = "0.10.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
dependencies = [
"generic-array",
]
[[package]]
name = "bytes"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cpufeatures"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "data-encoding"
version = "2.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010"
[[package]]
name = "digest"
version = "0.10.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "futures"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
dependencies = [
"futures-channel",
"futures-core",
"futures-executor",
"futures-io",
"futures-sink",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-channel"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
dependencies = [
"futures-core",
"futures-sink",
]
[[package]]
name = "futures-core"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
[[package]]
name = "futures-executor"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
[[package]]
name = "futures-macro"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-sink"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
[[package]]
name = "futures-task"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
[[package]]
name = "futures-util"
version = "0.3.31"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-macro",
"futures-sink",
"futures-task",
"memchr",
"pin-project-lite",
"pin-utils",
"slab",
]
[[package]]
name = "generic-array"
version = "0.14.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "getrandom"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73fea8450eea4bac3940448fb7ae50d91f034f941199fcd9d909a5a07aa455f0"
dependencies = [
"cfg-if",
"libc",
"r-efi",
"wasi 0.14.2+wasi-0.2.4",
]
[[package]]
name = "gimli"
version = "0.31.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "http"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4a85d31aea989eead29a3aaf9e1115a180df8282431156e533de47660892565"
dependencies = [
"bytes",
"fnv",
"itoa",
]
[[package]]
name = "httparse"
version = "1.10.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
[[package]]
name = "indoc"
version = "2.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd"
[[package]]
name = "itoa"
version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
[[package]]
name = "libc"
version = "0.2.171"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c19937216e9d3aa9956d9bb8dfc0b0c8beb6058fc4f7a4dc4d850edf86a237d6"
[[package]]
name = "lock_api"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
dependencies = [
"autocfg",
"scopeguard",
]
[[package]]
name = "log"
version = "0.4.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94"
[[package]]
name = "memchr"
version = "2.7.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "miniz_oxide"
version = "0.8.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3be647b768db090acb35d5ec5db2b0e1f1de11133ca123b9eacf5137868f892a"
dependencies = [
"adler2",
]
[[package]]
name = "mio"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
"wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys",
]
[[package]]
name = "object"
version = "0.36.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "62948e14d923ea95ea2c7c86c71013138b66525b86bdc08d2dcc262bdb497b87"
dependencies = [
"memchr",
]
[[package]]
name = "once_cell"
version = "1.21.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d"
[[package]]
name = "open-xiaoai"
version = "1.0.0"
dependencies = [
"futures",
"rand",
"serde",
"serde_json",
"tokio",
"tokio-tungstenite",
"uuid",
]
[[package]]
name = "open_xiaoai_server"
version = "1.0.0"
dependencies = [
"open-xiaoai",
"pyo3",
"pyo3-async-runtimes",
"serde",
"serde_json",
"tokio",
"tokio-tungstenite",
]
[[package]]
name = "parking_lot"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
]
[[package]]
name = "parking_lot_core"
version = "0.9.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8"
dependencies = [
"cfg-if",
"libc",
"redox_syscall",
"smallvec",
"windows-targets",
]
[[package]]
name = "pin-project-lite"
version = "0.2.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b"
[[package]]
name = "pin-utils"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "portable-atomic"
version = "1.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "350e9b48cbc6b0e028b0473b114454c6316e57336ee184ceab6e53f72c178b3e"
[[package]]
name = "ppv-lite86"
version = "0.2.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
dependencies = [
"zerocopy",
]
[[package]]
name = "proc-macro2"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84"
dependencies = [
"unicode-ident",
]
[[package]]
name = "pyo3"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "17da310086b068fbdcefbba30aeb3721d5bb9af8db4987d6735b2183ca567229"
dependencies = [
"cfg-if",
"indoc",
"libc",
"memoffset",
"once_cell",
"portable-atomic",
"pyo3-build-config",
"pyo3-ffi",
"pyo3-macros",
"unindent",
]
[[package]]
name = "pyo3-async-runtimes"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0b83dc42f9d41f50d38180dad65f0c99763b65a3ff2a81bf351dd35a1df8bf"
dependencies = [
"futures",
"once_cell",
"pin-project-lite",
"pyo3",
"pyo3-async-runtimes-macros",
"tokio",
]
[[package]]
name = "pyo3-async-runtimes-macros"
version = "0.24.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf103ba4062fbb1e8022d9ed9b9830fbab074b2db0a0496c78e45a62f4330bcd"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "pyo3-build-config"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e27165889bd793000a098bb966adc4300c312497ea25cf7a690a9f0ac5aa5fc1"
dependencies = [
"once_cell",
"target-lexicon",
]
[[package]]
name = "pyo3-ffi"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "05280526e1dbf6b420062f3ef228b78c0c54ba94e157f5cb724a609d0f2faabc"
dependencies = [
"libc",
"pyo3-build-config",
]
[[package]]
name = "pyo3-macros"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c3ce5686aa4d3f63359a5100c62a127c9f15e8398e5fdeb5deef1fed5cd5f44"
dependencies = [
"proc-macro2",
"pyo3-macros-backend",
"quote",
"syn",
]
[[package]]
name = "pyo3-macros-backend"
version = "0.24.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4cf6faa0cbfb0ed08e89beb8103ae9724eb4750e3a78084ba4017cbe94f3855"
dependencies = [
"heck",
"proc-macro2",
"pyo3-build-config",
"quote",
"syn",
]
[[package]]
name = "quote"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d"
dependencies = [
"proc-macro2",
]
[[package]]
name = "r-efi"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5"
[[package]]
name = "rand"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9fbfd9d094a40bf3ae768db9361049ace4c0e04a4fd6b359518bd7b73a73dd97"
dependencies = [
"rand_chacha",
"rand_core",
]
[[package]]
name = "rand_chacha"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb"
dependencies = [
"ppv-lite86",
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.9.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "99d9a13982dcf210057a8a78572b2217b667c3beacbf3a0d8b454f6f82837d38"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.5.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2f103c6d277498fbceb16e84d317e2a400f160f46904d5f5410848c829511a3"
dependencies = [
"bitflags",
]
[[package]]
name = "rustc-demangle"
version = "0.1.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "719b953e2095829ee67db738b3bfa9fa368c94900df327b3f07fe6e794d2fe1f"
[[package]]
name = "ryu"
version = "1.0.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f"
[[package]]
name = "scopeguard"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.140"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373"
dependencies = [
"itoa",
"memchr",
"ryu",
"serde",
]
[[package]]
name = "sha1"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1"
dependencies = [
"libc",
]
[[package]]
name = "slab"
version = "0.4.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
dependencies = [
"autocfg",
]
[[package]]
name = "smallvec"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8917285742e9f3e1683f0a9c4e6b57960b7314d0b08d30d1ecd426713ee2eee9"
[[package]]
name = "socket2"
version = "0.5.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4f5fd57c80058a56cf5c777ab8a126398ece8e442983605d280a44ce79d0edef"
dependencies = [
"libc",
"windows-sys",
]
[[package]]
name = "syn"
version = "2.0.100"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "target-lexicon"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e502f78cdbb8ba4718f566c418c52bc729126ffd16baee5baa718cf25dd5a69a"
[[package]]
name = "thiserror"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "2.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio"
version = "1.44.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6b88822cbe49de4185e3a4cbf8321dd487cf5fe0c5c65695fef6346371e9c48"
dependencies = [
"backtrace",
"bytes",
"libc",
"mio",
"parking_lot",
"pin-project-lite",
"signal-hook-registry",
"socket2",
"tokio-macros",
"windows-sys",
]
[[package]]
name = "tokio-macros"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "tokio-tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7a9daff607c6d2bf6c16fd681ccb7eecc83e4e2cdc1ca067ffaadfca5de7f084"
dependencies = [
"futures-util",
"log",
"tokio",
"tungstenite",
]
[[package]]
name = "tungstenite"
version = "0.26.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4793cb5e56680ecbb1d843515b23b6de9a75eb04b66643e256a396d43be33c13"
dependencies = [
"bytes",
"data-encoding",
"http",
"httparse",
"log",
"rand",
"sha1",
"thiserror",
"utf-8",
]
[[package]]
name = "typenum"
version = "1.18.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
[[package]]
name = "unicode-ident"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512"
[[package]]
name = "unindent"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7264e107f553ccae879d21fbea1d6724ac785e8c3bfc762137959b5802826ef3"
[[package]]
name = "utf-8"
version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "uuid"
version = "1.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9"
dependencies = [
"getrandom",
]
[[package]]
name = "version_check"
version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
[[package]]
name = "wasi"
version = "0.14.2+wasi-0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9683f9a5a998d873c0d21fcbe3c083009670149a8fab228644b8bd36b2c48cb3"
dependencies = [
"wit-bindgen-rt",
]
[[package]]
name = "windows-sys"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
dependencies = [
"windows-targets",
]
[[package]]
name = "windows-targets"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
dependencies = [
"windows_aarch64_gnullvm",
"windows_aarch64_msvc",
"windows_i686_gnu",
"windows_i686_gnullvm",
"windows_i686_msvc",
"windows_x86_64_gnu",
"windows_x86_64_gnullvm",
"windows_x86_64_msvc",
]
[[package]]
name = "windows_aarch64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
[[package]]
name = "windows_aarch64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
[[package]]
name = "windows_i686_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
[[package]]
name = "windows_i686_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
[[package]]
name = "windows_i686_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
[[package]]
name = "windows_x86_64_gnu"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
[[package]]
name = "windows_x86_64_gnullvm"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
[[package]]
name = "windows_x86_64_msvc"
version = "0.52.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]]
name = "wit-bindgen-rt"
version = "0.39.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1"
dependencies = [
"bitflags",
]
[[package]]
name = "zerocopy"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879"
dependencies = [
"zerocopy-derive",
]
[[package]]
name = "zerocopy-derive"
version = "0.8.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
+20
View File
@@ -0,0 +1,20 @@
[package]
name = "open_xiaoai_server"
version = "1.0.0"
edition = "2021"
[lib]
name = "open_xiaoai_server"
crate-type = ["cdylib"]
[dependencies]
pyo3 = { version = "0.24.0", features = ["extension-module", "abi3-py38"] }
pyo3-async-runtimes = { version = "0.24", features = [
"attributes",
"tokio-runtime",
] }
open-xiaoai = { path = "../../packages/client-rust" }
tokio = { version = "1.32.0", features = ["full"] }
tokio-tungstenite = "0.26.2"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
+57
View File
@@ -0,0 +1,57 @@
FROM python:3.12-slim AS builder
SHELL ["/bin/bash", "-o", "pipefail", "-c"]
# 更新源
RUN apt-get update && apt-get install -y --no-install-recommends \
curl build-essential libopus-dev \
&& rm -rf /var/lib/apt/lists/*
# 安装 Rust
RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
# 安装 uv
COPY --from=ghcr.io/astral-sh/uv:0.7 /uv /uvx /bin/
# 设置环境变量
ENV BASH_ENV=/root/.bash_env
RUN touch "$BASH_ENV"
RUN echo '. "$BASH_ENV"' >> "$HOME/.bashrc"
RUN echo '[ -s "$HOME/.cargo/env" ] && . "$HOME/.cargo/env"' >> "$BASH_ENV"
# 设置工作目录
WORKDIR /app
# 复制项目文件
COPY examples/xiaozhi .
COPY packages/client-rust ./client-rust
# 构建
RUN sed -i 's/\.\.\/\.\.\/packages\///g' Cargo.toml \
&& cargo build --release
# 安装依赖
RUN --mount=type=cache,target=/root/.cache/uv \
uv remove pyaudio && uv sync --locked --no-install-project --no-editable
# 构建 wheel 并安装
RUN uv run maturin build --release && uv remove maturin
FROM python:3.12-slim
WORKDIR /app
# 更新源
RUN apt-get update && apt-get install -y --no-install-recommends \
libopus-dev \
&& rm -rf /var/lib/apt/lists/*
ENV CLI=true
COPY --from=builder /app/.venv /app/.venv
COPY examples/xiaozhi/main.py .
COPY examples/xiaozhi/xiaozhi ./xiaozhi
# 先初始化关键词模型,然后启动主程序
CMD ["/bin/bash", "-c", "source /app/.venv/bin/activate && python xiaozhi/services/audio/kws/keywords.py && python main.py"]
+169
View File
@@ -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 运行
[![Docker Image Version](https://img.shields.io/docker/v/idootop/open-xiaoai-xiaozhi?color=%23086DCD&label=docker%20image)](https://hub.docker.com/r/idootop/open-xiaoai-xiaozhi)
推荐使用以下命令,直接 Docker 一键运行。
```shell
docker run -it --rm -p 4399:4399 -v $(pwd)/config.py:/app/config.py idootop/open-xiaoai-xiaozhi:latest
```
### 编译运行
为了能够正常编译运行该项目,你需要安装以下依赖环境/工具:
- uvhttps://github.com/astral-sh/uv
- Rust: https://www.rust-lang.org/learn/get-started
- [Opus](https://opus-codec.org/): 自行询问 AI 如何安装动态链接库,或参考[这篇文章](https://github.com/huangjunsen0406/py-xiaozhi/blob/3bfd2887244c510a13912c1d63263ae564a941e9/documents/docs/guide/01_%E7%B3%BB%E7%BB%9F%E4%BE%9D%E8%B5%96%E5%AE%89%E8%A3%85.md#2-opus-%E9%9F%B3%E9%A2%91%E7%BC%96%E8%A7%A3%E7%A0%81%E5%99%A8)
```bash
# 安装 Python 依赖
uv sync --locked
# 编译运行(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) 项目,特此鸣谢。
+72
View File
@@ -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",
]
+46
View File
@@ -0,0 +1,46 @@
#! /bin/sh
exec > /dev/null 2>&1
cat << 'EOF'
▄▖ ▖▖▘ ▄▖▄▖
▌▌▛▌█▌▛▌▚▘▌▀▌▛▌▌▌▐
▙▌▙▌▙▖▌▌▌▌▌█▌▙▌▛▌▟▖
v1.0.0 by: https://del.wang
EOF
# 等待能够正常访问 baidu.com
while ! ping -c 1 baidu.com > /dev/null 2>&1; do
echo "🤫 等待网络连接中..."
sleep 1
done
sleep 3
echo "✅ 网络连接成功"
DOWNLOAD_BASE_URL="https://gitee.com/idootop/artifacts/releases/download"
WORK_DIR="/data/open-xiaoai/scripts"
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$WORK_DIR/client-boot.sh" ]; then
curl -L -# -o "$WORK_DIR/client-boot.sh" "$DOWNLOAD_BASE_URL/open-xiaoai-client/boot.sh"
fi
if [ ! -f "$WORK_DIR/kws-boot.sh" ]; then
curl -L -# -o "$WORK_DIR/kws-boot.sh" "$DOWNLOAD_BASE_URL/open-xiaoai-kws/boot.sh"
fi
kill -9 `ps|grep "open-xiaoai/kws/monitor"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
sh "$WORK_DIR/kws-boot.sh" --no-monitor > /dev/null 2>&1 &
sh "$WORK_DIR/client-boot.sh"
+25
View File
@@ -0,0 +1,25 @@
#! /bin/sh
set -e
DOWNLOAD_BASE_URL="https://gitee.com/idootop/artifacts/releases/download"
WORK_DIR="/data/open-xiaoai/scripts"
if [ ! -d "$WORK_DIR" ]; then
mkdir -p "$WORK_DIR"
fi
if [ ! -f "$WORK_DIR/client.sh" ]; then
curl -L -# -o "$WORK_DIR/client.sh" "$DOWNLOAD_BASE_URL/open-xiaoai-client/init.sh"
fi
if [ ! -f "$WORK_DIR/kws.sh" ]; then
curl -L -# -o "$WORK_DIR/kws.sh" "$DOWNLOAD_BASE_URL/open-xiaoai-kws/init.sh"
fi
kill -9 `ps|grep "open-xiaoai/kws/monitor"|grep -v grep|awk '{print $1}'` > /dev/null 2>&1 || true
sh "$WORK_DIR/kws.sh" --no-monitor > /dev/null 2>&1 &
sh "$WORK_DIR/client.sh"
+56
View File
@@ -0,0 +1,56 @@
use open_xiaoai::services::connect::message::MessageManager;
use open_xiaoai::services::connect::rpc::RPC;
use pyo3::prelude::*;
use pyo3::types::PyBytes;
use serde_json::json;
use server::AppServer;
pub mod macros;
pub mod python;
pub mod server;
#[pyfunction]
fn on_output_data(py: Python, data: Py<PyBytes>) -> PyResult<Bound<PyAny>> {
let bytes = data.as_bytes(py).to_vec();
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let _ = MessageManager::instance()
.send_stream("play", bytes, None)
.await;
Ok(())
})
}
#[pyfunction]
fn start_server(py: Python) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async {
AppServer::run().await;
Ok(())
})
}
#[pyfunction]
fn run_shell(py: Python, script: String, timeout_millis: f64) -> PyResult<Bound<PyAny>> {
pyo3_async_runtimes::tokio::future_into_py(py, async move {
let res = RPC::instance()
.call_remote(
"run_shell",
Some(json!(script)),
Some(timeout_millis as u64),
)
.await;
let result = match res {
Err(e) => format!("run_shell error: {}", e),
Ok(res) => serde_json::to_string(&res.data.unwrap()).unwrap(),
};
Ok(result)
})
}
#[pymodule]
fn open_xiaoai_server(_py: Python, m: Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(start_server, &m)?)?;
m.add_function(wrap_pyfunction!(on_output_data, &m)?)?;
m.add_function(wrap_pyfunction!(run_shell, &m)?)?;
crate::python::init_module(&m)?;
Ok(())
}
+14
View File
@@ -0,0 +1,14 @@
/// 提供一个简便的日志记录宏,将消息传递给 Python 端
///
/// # 示例
///
/// ```
/// pylog!("这是一条日志消息");
/// pylog!("带有变量的日志: {}", variable);
/// ```
#[macro_export]
macro_rules! pylog {
($($arg:tt)*) => {
crate::python::PythonManager::instance().log(format!($($arg)*));
};
}
+77
View File
@@ -0,0 +1,77 @@
use pyo3::prelude::*;
use std::collections::HashMap;
use std::ffi::CString;
use std::sync::{Arc, LazyLock, RwLock};
pub struct PythonManager {
functions: Arc<RwLock<HashMap<String, PyObject>>>,
}
static INSTANCE: LazyLock<PythonManager> = LazyLock::new(PythonManager::new);
impl PythonManager {
pub fn new() -> Self {
Self {
functions: Arc::new(RwLock::new(HashMap::new())),
}
}
pub fn instance() -> &'static Self {
&INSTANCE
}
fn register_fn(&self, key: &str, function: PyObject) -> PyResult<()> {
let mut functions = self.functions.write().unwrap();
functions.insert(key.to_string(), function);
Ok(())
}
fn unregister_fn(&self, key: &str) -> PyResult<()> {
let mut functions = self.functions.write().unwrap();
functions.remove(key);
Ok(())
}
pub fn call_fn(&self, key: &str, arg: Option<PyObject>) -> PyResult<PyObject> {
let functions = self.functions.read().unwrap();
if let Some(function) = functions.get(key) {
Python::with_gil(|py| match arg {
None => function.call0(py),
Some(arg) => function.call1(py, (arg,)),
})
} else {
Err(pyo3::exceptions::PyKeyError::new_err(format!(
"未找到函数: {}",
key
)))
}
}
pub fn log(&self, text: String) {
let _ = self.eval(&format!("print('{}')", text));
}
pub fn eval(&self, script: &str) -> PyResult<()> {
Python::with_gil(|py| {
let c_script = CString::new(script)?;
py.run(&c_script, None, None)?;
Ok(())
})
}
}
#[pyfunction]
fn register_fn(key: &str, function: PyObject) -> PyResult<()> {
PythonManager::instance().register_fn(key, function)
}
#[pyfunction]
fn unregister_fn(key: &str) -> PyResult<()> {
PythonManager::instance().unregister_fn(key)
}
pub fn init_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(register_fn, m)?)?;
m.add_function(wrap_pyfunction!(unregister_fn, m)?)?;
Ok(())
}
+134
View File
@@ -0,0 +1,134 @@
use crate::python::PythonManager;
use open_xiaoai::base::{AppError, VERSION};
use open_xiaoai::services::audio::config::AudioConfig;
use open_xiaoai::services::connect::data::{Event, Request, Response, Stream};
use open_xiaoai::services::connect::handler::MessageHandler;
use open_xiaoai::services::connect::message::{MessageManager, WsStream};
use open_xiaoai::services::connect::rpc::RPC;
use open_xiaoai::services::speaker::SpeakerManager;
use open_xiaoai::utils::task::TaskManager;
use pyo3::types::PyBytes;
use pyo3::types::PyString;
use pyo3::Python;
use serde_json::json;
use tokio::net::{TcpListener, TcpStream};
use tokio_tungstenite::accept_async;
pub struct AppServer;
async fn test() -> Result<(), AppError> {
SpeakerManager::play_text("已连接").await?;
let _ = RPC::instance()
.call_remote(
"start_recording",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 16000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
let _ = RPC::instance()
.call_remote(
"start_play",
Some(json!(AudioConfig {
pcm: "noop".into(),
channels: 1,
bits_per_sample: 16,
sample_rate: 24000,
period_size: 1440 / 4,
buffer_size: 1440,
})),
None,
)
.await;
Ok(())
}
impl AppServer {
pub async fn connect(stream: TcpStream) -> Result<WsStream, AppError> {
let ws_stream = accept_async(stream).await?;
Ok(WsStream::Server(ws_stream))
}
pub async fn run() {
let addr = "0.0.0.0:4399";
let listener = TcpListener::bind(&addr)
.await
.expect(format!("❌ 绑定地址失败: {}", &addr).as_str());
crate::pylog!("✅ 已启动: {:?}", addr);
while let Ok((stream, addr)) = listener.accept().await {
// 同一时刻只处理一个连接
AppServer::handle_connection(stream, addr).await;
}
}
async fn handle_connection(stream: TcpStream, addr: std::net::SocketAddr) {
let Ok(ws_stream) = AppServer::connect(stream).await else {
crate::pylog!("❌ 连接异常: {}", addr);
return;
};
crate::pylog!("✅ 已连接: {:?}", addr);
AppServer::init(ws_stream).await;
if let Err(e) = MessageManager::instance().process_messages().await {
crate::pylog!("❌ 消息处理异常: {}", e);
}
AppServer::dispose().await;
crate::pylog!("❌ 已断开连接");
}
async fn init(ws_stream: WsStream) {
MessageManager::instance().init(ws_stream).await;
MessageHandler::<Event>::instance()
.set_handler(on_event)
.await;
MessageHandler::<Stream>::instance()
.set_handler(on_stream)
.await;
let rpc = RPC::instance();
rpc.add_command("get_version", get_version).await;
let test = tokio::spawn(async move {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let _ = test().await;
});
TaskManager::instance().add("test", test).await;
}
async fn dispose() {
MessageManager::instance().dispose().await;
TaskManager::instance().dispose("test").await;
}
}
async fn get_version(_: Request) -> Result<Response, AppError> {
let data = json!(VERSION.to_string());
Ok(Response::from_data(data))
}
async fn on_stream(stream: Stream) -> Result<(), AppError> {
let Stream { tag, bytes, .. } = stream;
match tag.as_str() {
"record" => {
let data = Python::with_gil(|py| PyBytes::new(py, &bytes).into());
PythonManager::instance().call_fn("on_input_data", Some(data))?;
}
_ => {}
}
Ok(())
}
async fn on_event(event: Event) -> Result<(), AppError> {
let event_json = serde_json::to_string(&event)?;
let data = Python::with_gil(|py| PyString::new(py, &event_json).into());
PythonManager::instance().call_fn("on_event", Some(data))?;
Ok(())
}
+163 -73
View File
@@ -20,39 +20,6 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/fc/bce832fd4fd99766c04d1ee0eead6b0ec6486fb100ae5e74c1d91292b982/certifi-2025.1.31-py3-none-any.whl", hash = "sha256:ca78db4565a652026a4db2bcdf68f2fb589ea80d0be70e03929ed730746b84fe", size = 166393 },
]
[[package]]
name = "cffi"
version = "1.17.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pycparser" },
]
sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178 },
{ url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840 },
{ url = "https://files.pythonhosted.org/packages/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803 },
{ url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850 },
{ url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729 },
{ url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256 },
{ url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424 },
{ url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568 },
{ url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736 },
{ url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448 },
{ url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976 },
{ url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989 },
{ url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802 },
{ url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792 },
{ url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893 },
{ url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810 },
{ url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200 },
{ url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447 },
{ url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358 },
{ url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469 },
{ url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475 },
{ url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009 },
]
[[package]]
name = "charset-normalizer"
version = "3.4.1"
@@ -89,38 +56,36 @@ wheels = [
]
[[package]]
name = "cryptography"
version = "44.0.2"
name = "coloredlogs"
version = "15.0.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
{ name = "humanfriendly" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/25/4ce80c78963834b8a9fd1cc1266be5ed8d1840785c0f2e1b73b8d128d505/cryptography-44.0.2.tar.gz", hash = "sha256:c63454aa261a0cf0c5b4718349629793e9e634993538db841165b3df74f37ec0", size = 710807 }
sdist = { url = "https://files.pythonhosted.org/packages/cc/c7/eed8f27100517e8c0e6b923d5f0845d0cb99763da6fdee00478f91db7325/coloredlogs-15.0.1.tar.gz", hash = "sha256:7c991aa71a4577af2f82600d8f8f3a89f936baeaf9b50a9c197da014e5bf16b0", size = 278520 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/92/ef/83e632cfa801b221570c5f58c0369db6fa6cef7d9ff859feab1aae1a8a0f/cryptography-44.0.2-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:efcfe97d1b3c79e486554efddeb8f6f53a4cdd4cf6086642784fa31fc384e1d7", size = 6676361 },
{ url = "https://files.pythonhosted.org/packages/30/ec/7ea7c1e4c8fc8329506b46c6c4a52e2f20318425d48e0fe597977c71dbce/cryptography-44.0.2-cp37-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29ecec49f3ba3f3849362854b7253a9f59799e3763b0c9d0826259a88efa02f1", size = 3952350 },
{ url = "https://files.pythonhosted.org/packages/27/61/72e3afdb3c5ac510330feba4fc1faa0fe62e070592d6ad00c40bb69165e5/cryptography-44.0.2-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc821e161ae88bfe8088d11bb39caf2916562e0a2dc7b6d56714a48b784ef0bb", size = 4166572 },
{ url = "https://files.pythonhosted.org/packages/26/e4/ba680f0b35ed4a07d87f9e98f3ebccb05091f3bf6b5a478b943253b3bbd5/cryptography-44.0.2-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:3c00b6b757b32ce0f62c574b78b939afab9eecaf597c4d624caca4f9e71e7843", size = 3958124 },
{ url = "https://files.pythonhosted.org/packages/9c/e8/44ae3e68c8b6d1cbc59040288056df2ad7f7f03bbcaca6b503c737ab8e73/cryptography-44.0.2-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:7bdcd82189759aba3816d1f729ce42ffded1ac304c151d0a8e89b9996ab863d5", size = 3678122 },
{ url = "https://files.pythonhosted.org/packages/27/7b/664ea5e0d1eab511a10e480baf1c5d3e681c7d91718f60e149cec09edf01/cryptography-44.0.2-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:4973da6ca3db4405c54cd0b26d328be54c7747e89e284fcff166132eb7bccc9c", size = 4191831 },
{ url = "https://files.pythonhosted.org/packages/2a/07/79554a9c40eb11345e1861f46f845fa71c9e25bf66d132e123d9feb8e7f9/cryptography-44.0.2-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:4e389622b6927d8133f314949a9812972711a111d577a5d1f4bee5e58736b80a", size = 3960583 },
{ url = "https://files.pythonhosted.org/packages/bb/6d/858e356a49a4f0b591bd6789d821427de18432212e137290b6d8a817e9bf/cryptography-44.0.2-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f514ef4cd14bb6fb484b4a60203e912cfcb64f2ab139e88c2274511514bf7308", size = 4191753 },
{ url = "https://files.pythonhosted.org/packages/b2/80/62df41ba4916067fa6b125aa8c14d7e9181773f0d5d0bd4dcef580d8b7c6/cryptography-44.0.2-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:1bc312dfb7a6e5d66082c87c34c8a62176e684b6fe3d90fcfe1568de675e6688", size = 4079550 },
{ url = "https://files.pythonhosted.org/packages/f3/cd/2558cc08f7b1bb40683f99ff4327f8dcfc7de3affc669e9065e14824511b/cryptography-44.0.2-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:3b721b8b4d948b218c88cb8c45a01793483821e709afe5f622861fc6182b20a7", size = 4298367 },
{ url = "https://files.pythonhosted.org/packages/71/59/94ccc74788945bc3bd4cf355d19867e8057ff5fdbcac781b1ff95b700fb1/cryptography-44.0.2-cp37-abi3-win32.whl", hash = "sha256:51e4de3af4ec3899d6d178a8c005226491c27c4ba84101bfb59c901e10ca9f79", size = 2772843 },
{ url = "https://files.pythonhosted.org/packages/ca/2c/0d0bbaf61ba05acb32f0841853cfa33ebb7a9ab3d9ed8bb004bd39f2da6a/cryptography-44.0.2-cp37-abi3-win_amd64.whl", hash = "sha256:c505d61b6176aaf982c5717ce04e87da5abc9a36a5b39ac03905c4aafe8de7aa", size = 3209057 },
{ url = "https://files.pythonhosted.org/packages/9e/be/7a26142e6d0f7683d8a382dd963745e65db895a79a280a30525ec92be890/cryptography-44.0.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:8e0ddd63e6bf1161800592c71ac794d3fb8001f2caebe0966e77c5234fa9efc3", size = 6677789 },
{ url = "https://files.pythonhosted.org/packages/06/88/638865be7198a84a7713950b1db7343391c6066a20e614f8fa286eb178ed/cryptography-44.0.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:81276f0ea79a208d961c433a947029e1a15948966658cf6710bbabb60fcc2639", size = 3951919 },
{ url = "https://files.pythonhosted.org/packages/d7/fc/99fe639bcdf58561dfad1faa8a7369d1dc13f20acd78371bb97a01613585/cryptography-44.0.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9a1e657c0f4ea2a23304ee3f964db058c9e9e635cc7019c4aa21c330755ef6fd", size = 4167812 },
{ url = "https://files.pythonhosted.org/packages/53/7b/aafe60210ec93d5d7f552592a28192e51d3c6b6be449e7fd0a91399b5d07/cryptography-44.0.2-cp39-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:6210c05941994290f3f7f175a4a57dbbb2afd9273657614c506d5976db061181", size = 3958571 },
{ url = "https://files.pythonhosted.org/packages/16/32/051f7ce79ad5a6ef5e26a92b37f172ee2d6e1cce09931646eef8de1e9827/cryptography-44.0.2-cp39-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1c3572526997b36f245a96a2b1713bf79ce99b271bbcf084beb6b9b075f29ea", size = 3679832 },
{ url = "https://files.pythonhosted.org/packages/78/2b/999b2a1e1ba2206f2d3bca267d68f350beb2b048a41ea827e08ce7260098/cryptography-44.0.2-cp39-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b042d2a275c8cee83a4b7ae30c45a15e6a4baa65a179a0ec2d78ebb90e4f6699", size = 4193719 },
{ url = "https://files.pythonhosted.org/packages/72/97/430e56e39a1356e8e8f10f723211a0e256e11895ef1a135f30d7d40f2540/cryptography-44.0.2-cp39-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:d03806036b4f89e3b13b6218fefea8d5312e450935b1a2d55f0524e2ed7c59d9", size = 3960852 },
{ url = "https://files.pythonhosted.org/packages/89/33/c1cf182c152e1d262cac56850939530c05ca6c8d149aa0dcee490b417e99/cryptography-44.0.2-cp39-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:c7362add18b416b69d58c910caa217f980c5ef39b23a38a0880dfd87bdf8cd23", size = 4193906 },
{ url = "https://files.pythonhosted.org/packages/e1/99/87cf26d4f125380dc674233971069bc28d19b07f7755b29861570e513650/cryptography-44.0.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:8cadc6e3b5a1f144a039ea08a0bdb03a2a92e19c46be3285123d32029f40a922", size = 4081572 },
{ url = "https://files.pythonhosted.org/packages/b3/9f/6a3e0391957cc0c5f84aef9fbdd763035f2b52e998a53f99345e3ac69312/cryptography-44.0.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:6f101b1f780f7fc613d040ca4bdf835c6ef3b00e9bd7125a4255ec574c7916e4", size = 4298631 },
{ url = "https://files.pythonhosted.org/packages/e2/a5/5bc097adb4b6d22a24dea53c51f37e480aaec3465285c253098642696423/cryptography-44.0.2-cp39-abi3-win32.whl", hash = "sha256:3dc62975e31617badc19a906481deacdeb80b4bb454394b4098e3f2525a488c5", size = 2773792 },
{ url = "https://files.pythonhosted.org/packages/33/cf/1f7649b8b9a3543e042d3f348e398a061923ac05b507f3f4d95f11938aa9/cryptography-44.0.2-cp39-abi3-win_amd64.whl", hash = "sha256:5f6f90b72d8ccadb9c6e311c775c8305381db88374c65fa1a68250aa8a9cb3a6", size = 3210957 },
{ url = "https://files.pythonhosted.org/packages/a7/06/3d6badcf13db419e25b07041d9c7b4a2c331d3f4e7134445ec5df57714cd/coloredlogs-15.0.1-py2.py3-none-any.whl", hash = "sha256:612ee75c546f53e92e70049c9dbfcc18c935a2b9a53b66085ce9ef6a6e5c0934", size = 46018 },
]
[[package]]
name = "flatbuffers"
version = "25.2.10"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e4/30/eb5dce7994fc71a2f685d98ec33cc660c0a5887db5610137e60d8cbc4489/flatbuffers-25.2.10.tar.gz", hash = "sha256:97e451377a41262f8d9bd4295cc836133415cc03d8cb966410a4af92eb00d26e", size = 22170 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/b8/25/155f9f080d5e4bc0082edfda032ea2bc2b8fab3f4d25d46c1e9dd22a1a89/flatbuffers-25.2.10-py2.py3-none-any.whl", hash = "sha256:ebba5f4d5ea615af3f7fd70fc310636fbb2bbd1f566ac0a23d98dd412de50051", size = 30953 },
]
[[package]]
name = "humanfriendly"
version = "10.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "pyreadline3", marker = "sys_platform == 'win32'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cc/3f/2c29224acb2e2df4d2046e4c73ee2662023c58ff5b113c4c1adac0886c43/humanfriendly-10.0.tar.gz", hash = "sha256:6b0b831ce8f15f7300721aa49829fc4e83921a9a301cc7f606be6686a2288ddc", size = 360702 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 },
]
[[package]]
@@ -152,6 +117,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/95/8379140838cd95472de843e982d0bf674e8dbf25a899c44e2f76b15704d9/maturin-1.8.3-py3-none-win_arm64.whl", hash = "sha256:33939aabf9a06a8a14ca6c399d32616c7e574fcca8d4ff6dcd984441051f32fb", size = 6687772 },
]
[[package]]
name = "mpmath"
version = "1.3.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198 },
]
[[package]]
name = "numpy"
version = "2.2.4"
@@ -191,37 +165,94 @@ wheels = [
]
[[package]]
name = "open-xiaoai-python"
name = "onnxruntime"
version = "1.22.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "coloredlogs" },
{ name = "flatbuffers" },
{ name = "numpy" },
{ name = "packaging" },
{ name = "protobuf" },
{ name = "sympy" },
]
wheels = [
{ url = "https://files.pythonhosted.org/packages/4d/de/9162872c6e502e9ac8c99a98a8738b2fab408123d11de55022ac4f92562a/onnxruntime-1.22.0-cp312-cp312-macosx_13_0_universal2.whl", hash = "sha256:f3c0380f53c1e72a41b3f4d6af2ccc01df2c17844072233442c3a7e74851ab97", size = 34298046 },
{ url = "https://files.pythonhosted.org/packages/03/79/36f910cd9fc96b444b0e728bba14607016079786adf032dae61f7c63b4aa/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8601128eaef79b636152aea76ae6981b7c9fc81a618f584c15d78d42b310f1c", size = 14443220 },
{ url = "https://files.pythonhosted.org/packages/8c/60/16d219b8868cc8e8e51a68519873bdb9f5f24af080b62e917a13fff9989b/onnxruntime-1.22.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6964a975731afc19dc3418fad8d4e08c48920144ff590149429a5ebe0d15fb3c", size = 16406377 },
{ url = "https://files.pythonhosted.org/packages/36/b4/3f1c71ce1d3d21078a6a74c5483bfa2b07e41a8d2b8fb1e9993e6a26d8d3/onnxruntime-1.22.0-cp312-cp312-win_amd64.whl", hash = "sha256:c0d534a43d1264d1273c2d4f00a5a588fa98d21117a3345b7104fa0bbcaadb9a", size = 12692233 },
{ url = "https://files.pythonhosted.org/packages/a9/65/5cb5018d5b0b7cba820d2c4a1d1b02d40df538d49138ba36a509457e4df6/onnxruntime-1.22.0-cp313-cp313-macosx_13_0_universal2.whl", hash = "sha256:fe7c051236aae16d8e2e9ffbfc1e115a0cc2450e873a9c4cb75c0cc96c1dae07", size = 34298715 },
{ url = "https://files.pythonhosted.org/packages/e1/89/1dfe1b368831d1256b90b95cb8d11da8ab769febd5c8833ec85ec1f79d21/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a6bbed10bc5e770c04d422893d3045b81acbbadc9fb759a2cd1ca00993da919", size = 14443266 },
{ url = "https://files.pythonhosted.org/packages/1e/70/342514ade3a33ad9dd505dcee96ff1f0e7be6d0e6e9c911fe0f1505abf42/onnxruntime-1.22.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fe45ee3e756300fccfd8d61b91129a121d3d80e9d38e01f03ff1295badc32b8", size = 16406707 },
{ url = "https://files.pythonhosted.org/packages/3e/89/2f64e250945fa87140fb917ba377d6d0e9122e029c8512f389a9b7f953f4/onnxruntime-1.22.0-cp313-cp313-win_amd64.whl", hash = "sha256:5a31d84ef82b4b05d794a4ce8ba37b0d9deb768fd580e36e17b39e0b4840253b", size = 12691777 },
{ url = "https://files.pythonhosted.org/packages/9f/48/d61d5f1ed098161edd88c56cbac49207d7b7b149e613d2cd7e33176c63b3/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a2ac5bd9205d831541db4e508e586e764a74f14efdd3f89af7fd20e1bf4a1ed", size = 14454003 },
{ url = "https://files.pythonhosted.org/packages/c3/16/873b955beda7bada5b0d798d3a601b2ff210e44ad5169f6d405b93892103/onnxruntime-1.22.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64845709f9e8a2809e8e009bc4c8f73b788cee9c6619b7d9930344eae4c9cd36", size = 16427482 },
]
[[package]]
name = "open-xiaoai-xiaozhi"
version = "1.0.0"
source = { editable = "." }
dependencies = [
{ name = "asyncio" },
{ name = "cryptography" },
{ name = "maturin" },
{ name = "numpy" },
{ name = "opuslib" },
{ name = "onnxruntime" },
{ name = "opuslib-next" },
{ name = "pyaudio" },
{ name = "pypinyin" },
{ name = "requests" },
{ name = "sentencepiece" },
{ name = "sherpa-onnx" },
{ name = "websockets" },
]
[package.metadata]
requires-dist = [
{ name = "asyncio", specifier = ">=3.4.3" },
{ name = "cryptography", specifier = ">=44.0.2" },
{ name = "maturin", specifier = ">=1.8.3" },
{ name = "numpy", specifier = ">=2.2.3" },
{ name = "opuslib", specifier = ">=3.0.1" },
{ name = "onnxruntime", specifier = ">=1.22.0" },
{ name = "opuslib-next", specifier = ">=1.1.4" },
{ name = "pyaudio", specifier = ">=0.2.14" },
{ name = "pypinyin", specifier = ">=0.54.0" },
{ name = "requests", specifier = ">=2.32.3" },
{ name = "sentencepiece", specifier = ">=0.2.0" },
{ name = "sherpa-onnx", specifier = ">=1.12.0" },
{ name = "websockets", specifier = ">=15.0.1" },
]
[[package]]
name = "opuslib"
version = "3.0.1"
name = "opuslib-next"
version = "1.1.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/46/55/826befabb29fd3902bad6d6d7308790894c7ad4d73f051728a0c53d37cd7/opuslib-3.0.1.tar.gz", hash = "sha256:2cb045e5b03e7fc50dfefe431e3404dddddbd8f5961c10c51e32dfb69a044c97", size = 8550 }
sdist = { url = "https://files.pythonhosted.org/packages/16/c9/0440b1c4f98c1bf1575ca3e3e3e18872c80d44c184b31d8d9ed1d04b9323/opuslib_next-1.1.4.tar.gz", hash = "sha256:415322be6c738ffef9df6cd7e79725880c1737ccf82c538af5c17b7bb6da60e9", size = 9363 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/a8/3674c6f3af230d692752cb2b2534c84c9e2f2e48b1221aa3354289e8cbea/opuslib_next-1.1.4-py3-none-any.whl", hash = "sha256:aefaf2ca1d2e0f279a4236045096d21f374581f2c33bf35657570e75028f8a55", size = 13435 },
]
[[package]]
name = "packaging"
version = "25.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469 },
]
[[package]]
name = "protobuf"
version = "6.30.2"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c8/8c/cf2ac658216eebe49eaedf1e06bc06cbf6a143469236294a1171a51357c3/protobuf-6.30.2.tar.gz", hash = "sha256:35c859ae076d8c56054c25b59e5e59638d86545ed6e2b6efac6be0b6ea3ba048", size = 429315 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/be/85/cd53abe6a6cbf2e0029243d6ae5fb4335da2996f6c177bb2ce685068e43d/protobuf-6.30.2-cp310-abi3-win32.whl", hash = "sha256:b12ef7df7b9329886e66404bef5e9ce6a26b54069d7f7436a0853ccdeb91c103", size = 419148 },
{ url = "https://files.pythonhosted.org/packages/97/e9/7b9f1b259d509aef2b833c29a1f3c39185e2bf21c9c1be1cd11c22cb2149/protobuf-6.30.2-cp310-abi3-win_amd64.whl", hash = "sha256:7653c99774f73fe6b9301b87da52af0e69783a2e371e8b599b3e9cb4da4b12b9", size = 431003 },
{ url = "https://files.pythonhosted.org/packages/8e/66/7f3b121f59097c93267e7f497f10e52ced7161b38295137a12a266b6c149/protobuf-6.30.2-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:0eb523c550a66a09a0c20f86dd554afbf4d32b02af34ae53d93268c1f73bc65b", size = 417579 },
{ url = "https://files.pythonhosted.org/packages/d0/89/bbb1bff09600e662ad5b384420ad92de61cab2ed0f12ace1fd081fd4c295/protobuf-6.30.2-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:50f32cc9fd9cb09c783ebc275611b4f19dfdfb68d1ee55d2f0c7fa040df96815", size = 317319 },
{ url = "https://files.pythonhosted.org/packages/28/50/1925de813499546bc8ab3ae857e3ec84efe7d2f19b34529d0c7c3d02d11d/protobuf-6.30.2-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4f6c687ae8efae6cf6093389a596548214467778146b7245e886f35e1485315d", size = 316212 },
{ url = "https://files.pythonhosted.org/packages/e5/a1/93c2acf4ade3c5b557d02d500b06798f4ed2c176fa03e3c34973ca92df7f/protobuf-6.30.2-py3-none-any.whl", hash = "sha256:ae86b030e69a98e08c77beab574cbcb9fff6d031d57209f574a5aea1445f4b51", size = 167062 },
]
[[package]]
name = "pyaudio"
@@ -236,12 +267,21 @@ wheels = [
]
[[package]]
name = "pycparser"
version = "2.22"
name = "pypinyin"
version = "0.54.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736 }
sdist = { url = "https://files.pythonhosted.org/packages/c9/7f/81cb5416ddacfeccca8eeedcd3543a72b093b26d9c4ca7bde8beea733e4e/pypinyin-0.54.0.tar.gz", hash = "sha256:9ab0d07ff51d191529e22134a60e109d0526d80b7a80afa73da4c89521610958", size = 837455 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552 },
{ url = "https://files.pythonhosted.org/packages/b6/ec/2c04ac863e7a85bb68b0b655cec2f19853d51d305ce3d785848db6037b8d/pypinyin-0.54.0-py2.py3-none-any.whl", hash = "sha256:5f776f19b9fd922e4121a114810b22048d90e6e8037fb1c07f4c40f987ae6e7a", size = 837012 },
]
[[package]]
name = "pyreadline3"
version = "3.5.4"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/0f/49/4cea918a08f02817aabae639e3d0ac046fef9f9180518a3ad394e22da148/pyreadline3-3.5.4.tar.gz", hash = "sha256:8d57d53039a1c75adba8e50dd3d992b28143480816187ea5efbd5c78e6c885b7", size = 99839 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/5a/dc/491b7661614ab97483abf2056be1deee4dc2490ecbf7bff9ab5cdbac86e1/pyreadline3-3.5.4-py3-none-any.whl", hash = "sha256:eaf8e6cc3c49bcccf145fc6067ba8643d1df34d604a1ec0eccbf7a18e6d3fae6", size = 83178 },
]
[[package]]
@@ -259,6 +299,56 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928 },
]
[[package]]
name = "sentencepiece"
version = "0.2.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/c9/d2/b9c7ca067c26d8ff085d252c89b5f69609ca93fb85a00ede95f4857865d4/sentencepiece-0.2.0.tar.gz", hash = "sha256:a52c19171daaf2e697dc6cbe67684e0fa341b1248966f6aebb541de654d15843", size = 2632106 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/27/5a/141b227ed54293360a9ffbb7bf8252b4e5efc0400cdeac5809340e5d2b21/sentencepiece-0.2.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:ea5f536e32ea8ec96086ee00d7a4a131ce583a1b18d130711707c10e69601cb2", size = 2409370 },
{ url = "https://files.pythonhosted.org/packages/2e/08/a4c135ad6fc2ce26798d14ab72790d66e813efc9589fd30a5316a88ca8d5/sentencepiece-0.2.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:d0cb51f53b6aae3c36bafe41e86167c71af8370a039f542c43b0cce5ef24a68c", size = 1239288 },
{ url = "https://files.pythonhosted.org/packages/49/0a/2fe387f825ac5aad5a0bfe221904882106cac58e1b693ba7818785a882b6/sentencepiece-0.2.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3212121805afc58d8b00ab4e7dd1f8f76c203ddb9dc94aa4079618a31cf5da0f", size = 1181597 },
{ url = "https://files.pythonhosted.org/packages/cc/38/e4698ee2293fe4835dc033c49796a39b3eebd8752098f6bd0aa53a14af1f/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2a3149e3066c2a75e0d68a43eb632d7ae728c7925b517f4c05c40f6f7280ce08", size = 1259220 },
{ url = "https://files.pythonhosted.org/packages/12/24/fd7ef967c9dad2f6e6e5386d0cadaf65cda8b7be6e3861a9ab3121035139/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:632f3594d3e7ac8b367bca204cb3fd05a01d5b21455acd097ea4c0e30e2f63d7", size = 1355962 },
{ url = "https://files.pythonhosted.org/packages/4f/d2/18246f43ca730bb81918f87b7e886531eda32d835811ad9f4657c54eee35/sentencepiece-0.2.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f295105c6bdbb05bd5e1b0cafbd78ff95036f5d3641e7949455a3f4e5e7c3109", size = 1301706 },
{ url = "https://files.pythonhosted.org/packages/8a/47/ca237b562f420044ab56ddb4c278672f7e8c866e183730a20e413b38a989/sentencepiece-0.2.0-cp312-cp312-win32.whl", hash = "sha256:fb89f811e5efd18bab141afc3fea3de141c3f69f3fe9e898f710ae7fe3aab251", size = 936941 },
{ url = "https://files.pythonhosted.org/packages/c6/97/d159c32642306ee2b70732077632895438867b3b6df282354bd550cf2a67/sentencepiece-0.2.0-cp312-cp312-win_amd64.whl", hash = "sha256:7a673a72aab81fef5ebe755c6e0cc60087d1f3a4700835d40537183c1703a45f", size = 991994 },
]
[[package]]
name = "sherpa-onnx"
version = "1.12.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/ab/81/64f21ee729ef0c21f645048c05201b0657b52ea2ada9e65ed2a1b69c1dd2/sherpa-onnx-1.12.0.tar.gz", hash = "sha256:91d912ad102bd4149ddd2dcb3a2bebfeecc34ad903eaeae89dc7fbf11103f62a", size = 530627 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/0a/50/3ff72177343d538341091102f9e139f4a0bbb89797ae78ecd4c479d015f5/sherpa_onnx-1.12.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0e7efc658647bcbd053879415755eff5fd96964d97ac3385be89c197a98182aa", size = 18166756 },
{ url = "https://files.pythonhosted.org/packages/bd/13/a7cfdd1edba5339c68a5de8c20e0d8621fef962ac4171a79772cefc79a8f/sherpa_onnx-1.12.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:cf62d2ad4a494debb2335298ac38f7ce98a4821ce3397a1d35248cb3978a51d9", size = 38783152 },
{ url = "https://files.pythonhosted.org/packages/25/ec/ebb543554538633d29171de2264fdf5fd8eb680c04c98e7fa9e249cb57a5/sherpa_onnx-1.12.0-cp312-cp312-macosx_11_0_x86_64.whl", hash = "sha256:5bc6bac071bf7f8f83113c760f932d154fd636f8a5b56fe97e199f7b95a18272", size = 20664664 },
{ url = "https://files.pythonhosted.org/packages/ae/1c/55d16fad62cfbe24239eb963cd3c9ba9906ad6c56772eaeab84211c7f1ab/sherpa_onnx-1.12.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4084a131bab3b6b5974b7c6472b09048c7f0b2d0f9b0b26171de8bff5bf91aa8", size = 23013320 },
{ url = "https://files.pythonhosted.org/packages/34/5f/2b1b736e745911b745d323e970b0452238af537a79b4d10f40e1326d642c/sherpa_onnx-1.12.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4d2078be1ef5d1d65822de21adca3b7e6353f3859dad02eb1cfd71c676e410ef", size = 24909403 },
{ url = "https://files.pythonhosted.org/packages/4c/88/06ef64d952c69c714c5f4fb22d83e86727f0567790663fd059446253ea72/sherpa_onnx-1.12.0-cp312-cp312-win32.whl", hash = "sha256:91d0aeaa1f6e5fb91b9b4a1326e67691a3f306a61a499a75f4c437697f8596ef", size = 20842198 },
{ url = "https://files.pythonhosted.org/packages/34/c1/e2ccebbf1931235850f734f420c0e719247648f53dc1011b528bb9bcef55/sherpa_onnx-1.12.0-cp312-cp312-win_amd64.whl", hash = "sha256:8df63f2235adc43fe735d07d0ca5e910337ebfeea03bc22931527b6a0231e505", size = 23667903 },
{ url = "https://files.pythonhosted.org/packages/ae/eb/4f8eeff8661933e8f6c69f44a5f29b49a1d7a156517737f0cf1c7dbfa54f/sherpa_onnx-1.12.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:15725f20a19ef9fb31c73e79a2aad53f2b962bef0f13ed15fd6b02c71f8c4904", size = 18166754 },
{ url = "https://files.pythonhosted.org/packages/29/00/57f967161782b78ebbbdd29ea54060e17cece2818a9e3f93cdee18739555/sherpa_onnx-1.12.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:74bc0bbc8695dff73acb620c0d3d4c5dd5d89aa0401d5cf31f6e15150b33947b", size = 38783224 },
{ url = "https://files.pythonhosted.org/packages/5c/0d/9186b91274e2d6711884bb88a6b3c8d5264d967e44c461839e90d9ceb931/sherpa_onnx-1.12.0-cp313-cp313-macosx_11_0_x86_64.whl", hash = "sha256:ced07e99b511b899d999d54f6f749c8200ef92df05cc6439dca8ca7da4f3800f", size = 20664831 },
{ url = "https://files.pythonhosted.org/packages/06/65/bb281e9309f83a7e69af403a06dc0ed6a96d9708adb486051d2dbdf782db/sherpa_onnx-1.12.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:111e270e7da787a578904268108de90284060a83c88721eac0de773407e84d97", size = 23013541 },
{ url = "https://files.pythonhosted.org/packages/ec/d4/2048c2e91881bdbb3139f60ccf52c511d8065d4a46ca102cbcd6081c79b7/sherpa_onnx-1.12.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:92a7ae7ce0d4b70417c03fdbcad8946eba739f2968688d87c501eea5bf97a909", size = 24909452 },
{ url = "https://files.pythonhosted.org/packages/42/d0/1485dd93cba8d018e82edbc21d63946e679f075335404367ed2ca9a4d5b0/sherpa_onnx-1.12.0-cp313-cp313-win32.whl", hash = "sha256:801b592a596c09a97e415813d5656a71a6f68655b000748f136cf66db69cbe4d", size = 20838807 },
{ url = "https://files.pythonhosted.org/packages/c4/5f/4a0cf4e8adc9d1aa299fbb87a379bc73737eb202bebccef9f0d09805e0d5/sherpa_onnx-1.12.0-cp313-cp313-win_amd64.whl", hash = "sha256:46cf8ccd5d360e1b02a2668d8416b746c624b78f8b7346f61d4e9fb6eab53c07", size = 23667855 },
]
[[package]]
name = "sympy"
version = "1.14.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "mpmath" },
]
sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 },
]
[[package]]
name = "urllib3"
version = "2.3.0"
+172
View File
@@ -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
+59
View File
@@ -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()

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