Compare commits

...

8 Commits

Author SHA1 Message Date
  Backpack 8bfb8be26f update readme3.0 2 months ago
  Backpack dd23b169de update readme 2 months ago
  Backpack 15434c3f2e finish scroll 2 months ago
  Backpack bda10ef218 send enter fix 2 months ago
  Backpack d1691ae0cb send enter 2 months ago
  Backpack 27a882d29e mouse control and click 2 months ago
  Backpack cd75372433 other gestures 2 months ago
  Backpack f74228e552 THREE_FINGERS_UP 2 months ago
25 changed files with 1895 additions and 115 deletions
Split View
  1. +9
    -0
      .gitignore
  2. +108
    -16
      README.md
  3. +5
    -4
      wavecontrol-test/index.html
  4. +754
    -1
      wavecontrol-test/package-lock.json
  5. +7
    -1
      wavecontrol-test/package.json
  6. +43
    -0
      wavecontrol-test/src-py/main.py
  7. +0
    -0
      wavecontrol-test/src-py/router/__init__.py
  8. BIN
      wavecontrol-test/src-py/router/__pycache__/__init__.cpython-312.pyc
  9. BIN
      wavecontrol-test/src-py/router/__pycache__/__init__.cpython-38.pyc
  10. BIN
      wavecontrol-test/src-py/router/__pycache__/ws.cpython-312.pyc
  11. BIN
      wavecontrol-test/src-py/router/__pycache__/ws.cpython-38.pyc
  12. +225
    -0
      wavecontrol-test/src-py/router/ws.py
  13. +41
    -24
      wavecontrol-test/src/hand_landmark/VideoDetector.vue
  14. +104
    -22
      wavecontrol-test/src/hand_landmark/detector.ts
  15. +341
    -5
      wavecontrol-test/src/hand_landmark/gesture_handler.ts
  16. +49
    -0
      wavecontrol-test/src/locales/en.ts
  17. +18
    -0
      wavecontrol-test/src/locales/i18n.ts
  18. +47
    -0
      wavecontrol-test/src/locales/zh.ts
  19. +10
    -4
      wavecontrol-test/src/main.ts
  20. +87
    -0
      wavecontrol-test/src/store/app.ts
  21. +7
    -0
      wavecontrol-test/src/vite-env.d copy.ts
  22. +9
    -0
      wavecontrol-test/test_mouse.py
  23. +0
    -15
      wavecontrol-test/tsconfig.app.json
  24. +28
    -5
      wavecontrol-test/tsconfig.json
  25. +3
    -18
      wavecontrol-test/tsconfig.node.json

+ 9
- 0
.gitignore View File

@ -0,0 +1,9 @@
.vscode/
node_modules/
src-py/__pycache__/
dist/
build/
model/
.vite/
src-tauri/
db.sqlite3

+ 108
- 16
README.md View File

@ -1,37 +1,129 @@
# 隔空手势识别系统
# WaveControl 手势识别测试模块
### 项目简介
> 本目录为主项目的**手势检测与识别子系统测试环境**,用于独立调试、验证基于 MediaPipe 的手部 landmark 检测与自定义手势逻辑的正确性。
本项目是一套基于**摄像头手势识别 + 语音识别**的非接触式人机交互系统,旨在帮助用户**在无需触碰设备的情况下完成对电脑的基本操作**。
------
灵感源于日常生活中“手脏无法点击”、“操作不便”的场景,结合后疫情时代对无接触操作的广泛需求,系统通过**手势识别模块**感知用户动作,通过**语音识别模块**解析用户指令,并联动前端和后端的控制逻辑,完成**系统级的键盘/鼠标模拟操作**,打造自然、高效的交互体验。
## 模块结构
```
wavecontrol-test/
├── public/ # 静态资源
├── src/ # 前端源代码(Vue3 + TS)
│ ├── components/ # UI组件(如 VideoDetector)
│ ├── hand_landmark/ # 核心手势识别模块(detector.ts、gesture_handler.ts)
│ └── locales/ # 多语言支持(如有)
├── src-py/ # Python 辅助测试脚本(如 test_mouse.py)
├── index.html # 主入口页面
├── main.ts # 启动脚本
├── vite.config.ts # 前端构建配置
├── tsconfig*.json # TypeScript 配置
└── README.md # 本文件
```
------
### 项目周期
## 功能说明
本项目为 2025 年《软件工程实践》课程的结课实践项目,采用**敏捷开发流程**,共分为四次迭代,逐步完成从原型设计到系统实现,最终交付一个可运行的完整系统,实现稳定、实用的**非接触式交互体验**。
模块聚焦**手部关键点检测 + 自定义手势识别逻辑的单元验证**,具备如下能力:
| 功能点 | 描述 |
| --------------- | ------------------------------------------------------------ |
| 摄像头接入 | 调用浏览器摄像头,获取实时图像流 |
| MediaPipe 集成 | 通过 `detector.ts` 接入 Google 手部识别模型,输出 21 个关键点数据 |
| 手势判断 | 通过 `gesture_handler.ts` 识别特定手势,如拇指上扬、握拳、转动角度等 |
| 可视化展示 | `VideoDetector.vue` 渲染摄像头画面 + landmark 点位,辅助调试 |
| 控制台输出 | 打印每帧检测结果(landmark 坐标、识别状态、FPS) |
| Python 模拟测试 | `test_mouse.py` 用于鼠标控制测试,独立于前端进行快速逻辑验证 |
### 项目成员
王云岱 朱子玥 杨嘉莉
## 实现手势功能一览
模块共识别并支持 **15种核心手势动作**,覆盖通用控制、游戏互动、音乐控制等多种场景,现阶段已完成如下识别逻辑与事件映射:
### 主控制系统
### 核心功能
| 序号 | 手势名称 | 动作说明 | 所属类型 |
| ---- | ------------ | ---------------------------------------------- | -------- |
| 01 | 光标控制 | 竖起食指滑动控制光标位置 | 通用控制 |
| 02 | 鼠标左键点击 | 食指 + 拇指上举触发点击事件 | 通用控制 |
| 03 | 滚动控制 | okay 手势(食指+拇指捏合)上下移动控制页面滚动 | 通用控制 |
| 04 | 全屏控制 | 四指并拢向上 → 触发设定键(默认 F 键) | 通用控制 |
| 05 | 退格 | 特定手势触发退格键 | 通用控制 |
| 06 | 开始语音识别 | 六指(含小指)手势启动语音识别 | 通用控制 |
| 07 | 结束语音识别 | 握拳触发语音识别终止 | 通用控制 |
| 08 | 暂停/继续 | 单手张开静止 1.5 秒,触发系统暂停或继续识别 | 通用控制 |
### 技术架构
### 游戏控制
| 序号 | 手势名称 | 动作说明 | 所属类型 |
| ---- | -------- | ------------------------------------------- | -------- |
| 09 | 向右移动 | 拇指上抬,其余手指收回 → 控制游戏角色向右走 | 游戏控制 |
| 10 | 跳跃 | 食指 + 中指上举,触发跳跃操作 | 游戏控制 |
| 11 | 右跳跃 | 拇指 + 食指 + 中指上举,触发右跳跃组合动作 | 游戏控制 |
### 应用场景
- 📺 追剧、吃饭时无需碰鼠标
- 👨‍🏫 教学演讲中通过手势控制幻灯片
- 👨‍🍳 厨房中翻菜谱、视频食谱不沾油手
- 🏥 医疗等无菌场景下控制系统界面
- 🤖 AR/智慧眼镜等未来设备交互前置尝试
### 音乐控制
| 序号 | 手势名称 | 动作说明 | 所属类型 |
| ---- | --------- | ------------------------------------ | -------- |
| 12 | 上一首 | 拇指向左摆动,控制播放器切换至上一首 | 音乐控制 |
| 13 | 下一首 | 拇指向右摆动,切换到下一首音乐 | 音乐控制 |
| 14 | 暂停/播放 | 比耶手势(✌️ / 🤘)控制音乐播放或暂停 | 音乐控制 |
### 模式切换
| 序号 | 手势名称 | 动作说明 | 所属类型 |
| ---- | ------------ | ---------------------------------------------- | -------- |
| 15 | 切换音乐模式 | rock 手势(🤘)触发音乐控制模式与普通模式的切换 | 模式切换 |
📌 **说明**
- 所有手势动作识别基于 `MediaPipe` 提供的 21 关键点;
- 每个手势映射事件已封装在 `gesture_handler.ts` 中;
- 可通过修改 `gesture_map` 动态扩展或调整触发逻辑;
- 模块支持平滑滤波、多帧确认机制,显著减少误识别。
## 快速启动(开发模式)
```
cd wavecontrol-test
npm install
npm run dev
```
> 默认运行在 http://localhost:5173,**页面为空白属正常现象**,请使用浏览器开发者工具调试。
------
## 调试指南
- 打开开发者工具(F12):
- **Console**:查看关键日志(手势判断结果、识别帧率等)
- **Network**:确认资源加载是否成功(如 MediaPipe 模型)
- **Elements**:手动挂载 `VideoDetector` 测试组件进行可视化验证
> 此模块**未接入主 UI 路由体系**,可作为“独立沙箱”运行,便于开发期间快速验证识别效果。
------
## 典型用例
- ✅ 验证 `gesture_handler.ts` 中的规则逻辑是否准确
- ✅ 查看 landmark 点是否能稳定输出(避免帧丢失)
- ✅ 测试新手势添加逻辑的正确性
- ✅ 结合 Python 脚本快速验证控制指令映射逻辑

+ 5
- 4
wavecontrol-test/index.html View File

@ -1,13 +1,14 @@
<!doctype html>
<html lang="en">
<html lang="en" style="height: 100%;width: 100%;">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Vite + Vue + TS</title>
<title>Tauri + Vue + Typescript App</title>
</head>
<body>
<div id="app"></div>
<body style="height: 100%;width: 100%;overflow: hidden;">
<div id="app" style="height: 100%;width: 100%"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>

+ 754
- 1
wavecontrol-test/package-lock.json View File

@ -12,8 +12,14 @@
"@tensorflow-models/handpose": "^0.1.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
"@tensorflow/tfjs-core": "^4.22.0",
"element-plus": "^2.10.4",
"fingerpose": "^0.1.0",
"vue": "^3.5.17"
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"pinia-shared-state": "^1.0.1",
"vue": "^3.5.17",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^24.0.12",
@ -57,6 +63,18 @@
"node": ">=6.0.0"
}
},
"node_modules/@babel/runtime": {
"version": "7.27.0",
"resolved": "https://registry.npmmirror.com/@babel/runtime/-/runtime-7.27.0.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==",
"license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": {
"node": ">=6.9.0"
}
},
"node_modules/@babel/types": {
"version": "7.28.0",
"resolved": "https://registry.npmmirror.com/@babel/types/-/types-7.28.0.tgz",
@ -70,6 +88,48 @@
"node": ">=6.9.0"
}
},
"node_modules/@css-render/plugin-bem": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/@css-render/plugin-bem/-/plugin-bem-0.15.14.tgz",
"integrity": "sha512-QK513CJ7yEQxm/P3EwsI+d+ha8kSOcjGvD6SevM41neEMxdULE+18iuQK6tEChAWMOQNQPLG/Rw3Khb69r5neg==",
"license": "MIT",
"peerDependencies": {
"css-render": "~0.15.14"
}
},
"node_modules/@css-render/vue3-ssr": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/@css-render/vue3-ssr/-/vue3-ssr-0.15.14.tgz",
"integrity": "sha512-//8027GSbxE9n3QlD73xFY6z4ZbHbvrOVB7AO6hsmrEzGbg+h2A09HboUyDgu+xsmj7JnvJD39Irt+2D0+iV8g==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@element-plus/icons-vue": {
"version": "2.3.1",
"resolved": "https://registry.npmmirror.com/@element-plus/icons-vue/-/icons-vue-2.3.1.tgz",
"integrity": "sha512-XxVUZv48RZAd87ucGS48jPf6pKu0yV5UCg9f4FFwtrYxXOwWuVJo6wOvSLKEoMQKjv8GsX/mhP6UsC1lRwbUWg==",
"license": "MIT",
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/@emotion/hash": {
"version": "0.8.0",
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.8.0.tgz",
"integrity": "sha512-kBJtf7PH6aWwZ6fka3zQ0p6SBYzx4fl1LoZXE2RrnYST9Xljm7WfKJrU4g/Xr3Beg72MLrp1AWNUmuYJTL7Cow==",
"license": "MIT"
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.6",
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.25.6.tgz",
@ -512,18 +572,104 @@
"node": ">=18"
}
},
"node_modules/@floating-ui/core": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@floating-ui/core/-/core-1.7.2.tgz",
"integrity": "sha512-wNB5ooIKHQc+Kui96jE/n69rHFWAVoxn5CAzL1Xdd8FG03cgY3MLO+GF9U3W737fYDSgPWA6MReKhBQBop6Pcw==",
"license": "MIT",
"dependencies": {
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/dom": {
"version": "1.7.2",
"resolved": "https://registry.npmmirror.com/@floating-ui/dom/-/dom-1.7.2.tgz",
"integrity": "sha512-7cfaOQuCS27HD7DX+6ib2OrnW+b4ZBwDNnCcT0uTyidcmyWb03FnQqJybDBoCnpdxwBSfA94UAYlRCt7mV+TbA==",
"license": "MIT",
"dependencies": {
"@floating-ui/core": "^1.7.2",
"@floating-ui/utils": "^0.2.10"
}
},
"node_modules/@floating-ui/utils": {
"version": "0.2.10",
"resolved": "https://registry.npmmirror.com/@floating-ui/utils/-/utils-0.2.10.tgz",
"integrity": "sha512-aGTxbpbg8/b5JfU1HXSrbH3wXZuLPJcNEcZQFMxLs3oSzgtVu6nFPkbbGGUvBcUjKV2YyB9Wxxabo+HEH9tcRQ==",
"license": "MIT"
},
"node_modules/@intlify/core-base": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/@intlify/core-base/-/core-base-9.14.4.tgz",
"integrity": "sha512-vtZCt7NqWhKEtHa3SD/322DlgP5uR9MqWxnE0y8Q0tjDs9H5Lxhss+b5wv8rmuXRoHKLESNgw9d+EN9ybBbj9g==",
"license": "MIT",
"dependencies": {
"@intlify/message-compiler": "9.14.4",
"@intlify/shared": "9.14.4"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/message-compiler": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/@intlify/message-compiler/-/message-compiler-9.14.4.tgz",
"integrity": "sha512-vcyCLiVRN628U38c3PbahrhbbXrckrM9zpy0KZVlDk2Z0OnGwv8uQNNXP3twwGtfLsCf4gu3ci6FMIZnPaqZsw==",
"license": "MIT",
"dependencies": {
"@intlify/shared": "9.14.4",
"source-map-js": "^1.0.2"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@intlify/shared": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/@intlify/shared/-/shared-9.14.4.tgz",
"integrity": "sha512-P9zv6i1WvMc9qDBWvIgKkymjY2ptIiQ065PjDv7z7fDqH3J/HBRBN5IoiR46r/ujRcU7hCuSIZWvCAFCyuOYZA==",
"license": "MIT",
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
}
},
"node_modules/@jridgewell/sourcemap-codec": {
"version": "1.5.4",
"resolved": "https://registry.npmmirror.com/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.4.tgz",
"integrity": "sha512-VT2+G1VQs/9oz078bLrYbecdZKs912zQlkelYpuf+SXF+QvZDYJlbx/LSx+meSAwdDFnF8FVXW92AVjjkVmgFw==",
"license": "MIT"
},
"node_modules/@juggle/resize-observer": {
"version": "3.4.0",
"resolved": "https://registry.npmmirror.com/@juggle/resize-observer/-/resize-observer-3.4.0.tgz",
"integrity": "sha512-dfLbk+PwWvFzSxwk3n5ySL0hfBog779o8h68wK/7/APo/7cgyWp5jcXockbxdk5kFRkbeXWm4Fbi9FrdN381sA==",
"license": "Apache-2.0"
},
"node_modules/@mediapipe/tasks-vision": {
"version": "0.10.22-rc.20250304",
"resolved": "https://registry.npmmirror.com/@mediapipe/tasks-vision/-/tasks-vision-0.10.22-rc.20250304.tgz",
"integrity": "sha512-dElxVXMFGthshfIj+qAVm8KE2jmNo2p8oXFib8WzEjb7GNaX/ClWBc8UJfoSZwjEMVrdHJ4YUfa7P3ifl6MIWw==",
"license": "Apache-2.0"
},
"node_modules/@popperjs/core": {
"name": "@sxzz/popperjs-es",
"version": "2.11.7",
"resolved": "https://registry.npmmirror.com/@sxzz/popperjs-es/-/popperjs-es-2.11.7.tgz",
"integrity": "sha512-Ccy0NlLkzr0Ex2FKvh2X+OyERHXJ88XJ1MXtsI9y9fGexlaXaVTPzBCRBwIxFkORuOb+uBqeu+RqnpgYTEZRUQ==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/popperjs"
}
},
"node_modules/@rolldown/pluginutils": {
"version": "1.0.0-beta.19",
"resolved": "https://registry.npmmirror.com/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.19.tgz",
@ -901,6 +1047,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/@types/katex": {
"version": "0.16.7",
"resolved": "https://registry.npmmirror.com/@types/katex/-/katex-0.16.7.tgz",
"integrity": "sha512-HMwFiRujE5PjrgwHQ25+bsLJgowjGjm5Z8FVSf0N6PwgJrwxH0QxzHYDcKsTfV3wva0vzrpqMTJS2jXPr5BMEQ==",
"license": "MIT"
},
"node_modules/@types/lodash": {
"version": "4.17.20",
"resolved": "https://registry.npmmirror.com/@types/lodash/-/lodash-4.17.20.tgz",
"integrity": "sha512-H3MHACvFUEiujabxhaI/ImO6gUrd8oOurg7LQtS7mbwIXA/cUqWrvBsaeJ23aZEPk1TAYkurjfMbSELfoCXlGA==",
"license": "MIT"
},
"node_modules/@types/lodash-es": {
"version": "4.17.12",
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT",
"dependencies": {
"@types/lodash": "*"
}
},
"node_modules/@types/long": {
"version": "4.0.2",
"resolved": "https://registry.npmmirror.com/@types/long/-/long-4.0.2.tgz",
@ -929,6 +1096,12 @@
"integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A==",
"license": "MIT"
},
"node_modules/@types/web-bluetooth": {
"version": "0.0.16",
"resolved": "https://registry.npmmirror.com/@types/web-bluetooth/-/web-bluetooth-0.0.16.tgz",
"integrity": "sha512-oh8q2Zc32S6gd/j50GowEjKLoOVOwHP/bWVjKJInBwQqdOYMdPrf1oVlelTlyfFK3CKxL1uahMDAr+vy8T7yMQ==",
"license": "MIT"
},
"node_modules/@vitejs/plugin-vue": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/@vitejs/plugin-vue/-/plugin-vue-6.0.0.tgz",
@ -1036,6 +1209,39 @@
"he": "^1.2.0"
}
},
"node_modules/@vue/devtools-api": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-7.7.7.tgz",
"integrity": "sha512-lwOnNBH2e7x1fIIbVT7yF5D+YWhqELm55/4ZKf45R9T8r9dE2AIOy8HKjfqzGsoTHFbWbr337O4E0A0QADnjBg==",
"license": "MIT",
"dependencies": {
"@vue/devtools-kit": "^7.7.7"
}
},
"node_modules/@vue/devtools-kit": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-kit/-/devtools-kit-7.7.7.tgz",
"integrity": "sha512-wgoZtxcTta65cnZ1Q6MbAfePVFxfM+gq0saaeytoph7nEa7yMXoi6sCPy4ufO111B9msnw0VOWjPEFCXuAKRHA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-shared": "^7.7.7",
"birpc": "^2.3.0",
"hookable": "^5.5.3",
"mitt": "^3.0.1",
"perfect-debounce": "^1.0.0",
"speakingurl": "^14.0.1",
"superjson": "^2.2.2"
}
},
"node_modules/@vue/devtools-shared": {
"version": "7.7.7",
"resolved": "https://registry.npmmirror.com/@vue/devtools-shared/-/devtools-shared-7.7.7.tgz",
"integrity": "sha512-+udSj47aRl5aKb0memBvcUG9koarqnxNM5yjuREvqwK6T3ap4mn3Zqqc17QrBFTqSMjr3HK1cvStEZpMDpfdyw==",
"license": "MIT",
"dependencies": {
"rfdc": "^1.4.1"
}
},
"node_modules/@vue/language-core": {
"version": "2.2.12",
"resolved": "https://registry.npmmirror.com/@vue/language-core/-/language-core-2.2.12.tgz",
@ -1130,6 +1336,94 @@
}
}
},
"node_modules/@vueuse/core": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/core/-/core-9.13.0.tgz",
"integrity": "sha512-pujnclbeHWxxPRqXWmdkKV5OX4Wk4YeK7wusHqRwU0Q7EFusHoqNA/aPhB6KCh9hEqJkLAJo7bb0Lh9b+OIVzw==",
"license": "MIT",
"dependencies": {
"@types/web-bluetooth": "^0.0.16",
"@vueuse/metadata": "9.13.0",
"@vueuse/shared": "9.13.0",
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/core/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@vueuse/metadata": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/metadata/-/metadata-9.13.0.tgz",
"integrity": "sha512-gdU7TKNAUVlXXLbaF+ZCfte8BjRJQWPCa2J55+7/h+yDtzw3vOoGQDRXzI6pyKyo6bXFT5/QoPE4hAknExjRLQ==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared": {
"version": "9.13.0",
"resolved": "https://registry.npmmirror.com/@vueuse/shared/-/shared-9.13.0.tgz",
"integrity": "sha512-UrnhU+Cnufu4S6JLCPZnkWh0WwZGUp72ktOF2DFptMlOs3TOdVv8xJN53zhHGARmVOsz5KqOls09+J1NR6sBKw==",
"license": "MIT",
"dependencies": {
"vue-demi": "*"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/@vueuse/shared/node_modules/vue-demi": {
"version": "0.14.10",
"resolved": "https://registry.npmmirror.com/vue-demi/-/vue-demi-0.14.10.tgz",
"integrity": "sha512-nMZBOwuzabUO0nLgIcc6rycZEebF6eeUfaiQx9+WSk8e29IbLvPU9feI6tqW4kTo3hvoYAJkMh8n8D0fuISphg==",
"hasInstallScript": true,
"license": "MIT",
"bin": {
"vue-demi-fix": "bin/vue-demi-fix.js",
"vue-demi-switch": "bin/vue-demi-switch.js"
},
"engines": {
"node": ">=12"
},
"funding": {
"url": "https://github.com/sponsors/antfu"
},
"peerDependencies": {
"@vue/composition-api": "^1.0.0-rc.1",
"vue": "^3.0.0-0 || ^2.6.0"
},
"peerDependenciesMeta": {
"@vue/composition-api": {
"optional": true
}
}
},
"node_modules/@webgpu/types": {
"version": "0.1.38",
"resolved": "https://registry.npmmirror.com/@webgpu/types/-/types-0.1.38.tgz",
@ -1143,12 +1437,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/async-validator": {
"version": "4.2.5",
"resolved": "https://registry.npmmirror.com/async-validator/-/async-validator-4.2.5.tgz",
"integrity": "sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==",
"license": "MIT"
},
"node_modules/balanced-match": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/balanced-match/-/balanced-match-1.0.2.tgz",
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"license": "MIT"
},
"node_modules/birpc": {
"version": "2.4.0",
"resolved": "https://registry.npmmirror.com/birpc/-/birpc-2.4.0.tgz",
"integrity": "sha512-5IdNxTyhXHv2UlgnPHQ0h+5ypVmkrYHzL8QT+DwFZ//2N/oNV8Ch+BCRmTJ3x6/z9Axo/cXYBc9eprsUVK/Jsg==",
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/antfu"
}
},
"node_modules/brace-expansion": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/brace-expansion/-/brace-expansion-2.0.2.tgz",
@ -1159,18 +1468,89 @@
"balanced-match": "^1.0.0"
}
},
"node_modules/broadcast-channel": {
"version": "7.1.0",
"resolved": "https://registry.npmmirror.com/broadcast-channel/-/broadcast-channel-7.1.0.tgz",
"integrity": "sha512-InJljddsYWbEL8LBnopnCg+qMQp9KcowvYWOt4YWrjD5HmxzDYKdVbDS1w/ji5rFZdRD58V5UxJPtBdpEbEJYw==",
"license": "MIT",
"dependencies": {
"@babel/runtime": "7.27.0",
"oblivious-set": "1.4.0",
"p-queue": "6.6.2",
"unload": "2.4.1"
},
"funding": {
"url": "https://github.com/sponsors/pubkey"
}
},
"node_modules/concat-map": {
"version": "0.0.1",
"resolved": "https://registry.npmmirror.com/concat-map/-/concat-map-0.0.1.tgz",
"integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==",
"license": "MIT"
},
"node_modules/copy-anything": {
"version": "3.0.5",
"resolved": "https://registry.npmmirror.com/copy-anything/-/copy-anything-3.0.5.tgz",
"integrity": "sha512-yCEafptTtb4bk7GLEQoM8KVJpxAfdBJYaXyzQEgQQQgYrZiDp8SJmGKlYza6CYjEDNstAdNdKA3UuoULlEbS6w==",
"license": "MIT",
"dependencies": {
"is-what": "^4.1.8"
},
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/css-render": {
"version": "0.15.14",
"resolved": "https://registry.npmmirror.com/css-render/-/css-render-0.15.14.tgz",
"integrity": "sha512-9nF4PdUle+5ta4W5SyZdLCCmFd37uVimSjg1evcTqKJCyvCEEj12WKzOSBNak6r4im4J4iYXKH1OWpUV5LBYFg==",
"license": "MIT",
"dependencies": {
"@emotion/hash": "~0.8.0",
"csstype": "~3.0.5"
}
},
"node_modules/css-render/node_modules/csstype": {
"version": "3.0.11",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.0.11.tgz",
"integrity": "sha512-sa6P2wJ+CAbgyy4KFssIb/JNMLxFvKF1pCYCSXS8ZMuqZnMsrxqI2E5sPyoTpxoPU/gVZMzr2zjOfg8GIZOMsw==",
"license": "MIT"
},
"node_modules/csstype": {
"version": "3.1.3",
"resolved": "https://registry.npmmirror.com/csstype/-/csstype-3.1.3.tgz",
"integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==",
"license": "MIT"
},
"node_modules/date-fns": {
"version": "3.6.0",
"resolved": "https://registry.npmmirror.com/date-fns/-/date-fns-3.6.0.tgz",
"integrity": "sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/kossnocorp"
}
},
"node_modules/date-fns-tz": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/date-fns-tz/-/date-fns-tz-3.2.0.tgz",
"integrity": "sha512-sg8HqoTEulcbbbVXeg84u5UnlsQa8GS5QXMqjjYIhS4abEVVKIUwe0/l/UhrZdKaL/W5eWZNlbTeEIiOXTcsBQ==",
"license": "MIT",
"peerDependencies": {
"date-fns": "^3.0.0 || ^4.0.0"
}
},
"node_modules/dayjs": {
"version": "1.11.13",
"resolved": "https://registry.npmmirror.com/dayjs/-/dayjs-1.11.13.tgz",
"integrity": "sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==",
"license": "MIT"
},
"node_modules/de-indent": {
"version": "1.0.2",
"resolved": "https://registry.npmmirror.com/de-indent/-/de-indent-1.0.2.tgz",
@ -1178,6 +1558,32 @@
"dev": true,
"license": "MIT"
},
"node_modules/element-plus": {
"version": "2.10.4",
"resolved": "https://registry.npmmirror.com/element-plus/-/element-plus-2.10.4.tgz",
"integrity": "sha512-UD4elWHrCnp1xlPhbXmVcaKFLCRaRAY6WWRwemGfGW3ceIjXm9fSYc9RNH3AiOEA6Ds1p9ZvhCs76CR9J8Vd+A==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.1",
"@element-plus/icons-vue": "^2.3.1",
"@floating-ui/dom": "^1.0.1",
"@popperjs/core": "npm:@sxzz/popperjs-es@^2.11.7",
"@types/lodash": "^4.14.182",
"@types/lodash-es": "^4.17.6",
"@vueuse/core": "^9.1.0",
"async-validator": "^4.2.5",
"dayjs": "^1.11.13",
"escape-html": "^1.0.3",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"lodash-unified": "^1.0.2",
"memoize-one": "^6.0.0",
"normalize-wheel-es": "^1.2.0"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/entities": {
"version": "4.5.0",
"resolved": "https://registry.npmmirror.com/entities/-/entities-4.5.0.tgz",
@ -1232,12 +1638,30 @@
"@esbuild/win32-x64": "0.25.6"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
"license": "MIT"
},
"node_modules/estree-walker": {
"version": "2.0.2",
"resolved": "https://registry.npmmirror.com/estree-walker/-/estree-walker-2.0.2.tgz",
"integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==",
"license": "MIT"
},
"node_modules/eventemitter3": {
"version": "4.0.7",
"resolved": "https://registry.npmmirror.com/eventemitter3/-/eventemitter3-4.0.7.tgz",
"integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==",
"license": "MIT"
},
"node_modules/evtd": {
"version": "0.2.4",
"resolved": "https://registry.npmmirror.com/evtd/-/evtd-0.2.4.tgz",
"integrity": "sha512-qaeGN5bx63s/AXgQo8gj6fBkxge+OoLddLniox5qtLAEY5HSnuSlISXVPxnSae1dWblvTh4/HoMIB+mbMsvZzw==",
"license": "MIT"
},
"node_modules/fdir": {
"version": "6.4.6",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.4.6.tgz",
@ -1333,6 +1757,21 @@
"he": "bin/he"
}
},
"node_modules/highlight.js": {
"version": "11.11.1",
"resolved": "https://registry.npmmirror.com/highlight.js/-/highlight.js-11.11.1.tgz",
"integrity": "sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/hookable": {
"version": "5.5.3",
"resolved": "https://registry.npmmirror.com/hookable/-/hookable-5.5.3.tgz",
"integrity": "sha512-Yc+BQe8SvoXH1643Qez1zqLRmbA5rCL+sSmk6TVos0LWVfNIB7PGncdlId77WzLGSIB5KaWgTaNTs2lNVEI6VQ==",
"license": "MIT"
},
"node_modules/inflight": {
"version": "1.0.6",
"resolved": "https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz",
@ -1350,6 +1789,41 @@
"integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
"license": "ISC"
},
"node_modules/is-what": {
"version": "4.1.16",
"resolved": "https://registry.npmmirror.com/is-what/-/is-what-4.1.16.tgz",
"integrity": "sha512-ZhMwEosbFJkA0YhFnNDgTM4ZxDRsS6HqTo7qsZM08fehyRYIYa0yHu5R6mgo1n/8MgaPBXiPimPD77baVFYg+A==",
"license": "MIT",
"engines": {
"node": ">=12.13"
},
"funding": {
"url": "https://github.com/sponsors/mesqueeb"
}
},
"node_modules/lodash": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT"
},
"node_modules/lodash-es": {
"version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT"
},
"node_modules/lodash-unified": {
"version": "1.0.3",
"resolved": "https://registry.npmmirror.com/lodash-unified/-/lodash-unified-1.0.3.tgz",
"integrity": "sha512-WK9qSozxXOD7ZJQlpSqOT+om2ZfcT4yO+03FuzAHD0wF6S0l0090LRPDx3vhTTLZ8cFKpBn+IOcVXK6qOcIlfQ==",
"license": "MIT",
"peerDependencies": {
"@types/lodash-es": "*",
"lodash": "*",
"lodash-es": "*"
}
},
"node_modules/long": {
"version": "4.0.0",
"resolved": "https://registry.npmmirror.com/long/-/long-4.0.0.tgz",
@ -1365,6 +1839,12 @@
"@jridgewell/sourcemap-codec": "^1.5.0"
}
},
"node_modules/memoize-one": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/memoize-one/-/memoize-one-6.0.0.tgz",
"integrity": "sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==",
"license": "MIT"
},
"node_modules/minimatch": {
"version": "9.0.5",
"resolved": "https://registry.npmmirror.com/minimatch/-/minimatch-9.0.5.tgz",
@ -1381,6 +1861,12 @@
"url": "https://github.com/sponsors/isaacs"
}
},
"node_modules/mitt": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/mitt/-/mitt-3.0.1.tgz",
"integrity": "sha512-vKivATfr97l2/QBCYAkXYDbrIWPM2IIKEl7YPhjCvKlG3kE2gm+uBo6nEXK3M5/Ffh/FLpKExzOQ3JJoJGFKBw==",
"license": "MIT"
},
"node_modules/muggle-string": {
"version": "0.4.1",
"resolved": "https://registry.npmmirror.com/muggle-string/-/muggle-string-0.4.1.tgz",
@ -1388,6 +1874,36 @@
"dev": true,
"license": "MIT"
},
"node_modules/naive-ui": {
"version": "2.42.0",
"resolved": "https://registry.npmmirror.com/naive-ui/-/naive-ui-2.42.0.tgz",
"integrity": "sha512-c7cXR2YgOjgtBadXHwiWL4Y0tpGLAI5W5QzzHksOi22iuHXoSGMAzdkVTGVPE/PM0MSGQ/JtUIzCx2Y0hU0vTQ==",
"license": "MIT",
"dependencies": {
"@css-render/plugin-bem": "^0.15.14",
"@css-render/vue3-ssr": "^0.15.14",
"@types/katex": "^0.16.2",
"@types/lodash": "^4.14.198",
"@types/lodash-es": "^4.17.9",
"async-validator": "^4.2.5",
"css-render": "^0.15.14",
"csstype": "^3.1.3",
"date-fns": "^3.6.0",
"date-fns-tz": "^3.1.3",
"evtd": "^0.2.4",
"highlight.js": "^11.8.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.21",
"seemly": "^0.3.8",
"treemate": "^0.3.11",
"vdirs": "^0.1.8",
"vooks": "^0.2.12",
"vueuc": "^0.4.63"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/nanoid": {
"version": "3.3.11",
"resolved": "https://registry.npmmirror.com/nanoid/-/nanoid-3.3.11.tgz",
@ -1426,6 +1942,21 @@
}
}
},
"node_modules/normalize-wheel-es": {
"version": "1.2.0",
"resolved": "https://registry.npmmirror.com/normalize-wheel-es/-/normalize-wheel-es-1.2.0.tgz",
"integrity": "sha512-Wj7+EJQ8mSuXr2iWfnujrimU35R2W4FAErEyTmJoJ7ucwTn2hOUSsRehMb5RSYkxXGTM7Y9QpvPmp++w5ftoJw==",
"license": "BSD-3-Clause"
},
"node_modules/oblivious-set": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/oblivious-set/-/oblivious-set-1.4.0.tgz",
"integrity": "sha512-szyd0ou0T8nsAqHtprRcP3WidfsN1TnAR5yWXf2mFCEr5ek3LEOkT6EZ/92Xfs74HIdyhG5WkGxIssMU0jBaeg==",
"license": "MIT",
"engines": {
"node": ">=16"
}
},
"node_modules/once": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/once/-/once-1.4.0.tgz",
@ -1435,6 +1966,43 @@
"wrappy": "1"
}
},
"node_modules/p-finally": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/p-finally/-/p-finally-1.0.0.tgz",
"integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmmirror.com/p-queue/-/p-queue-6.6.2.tgz",
"integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==",
"license": "MIT",
"dependencies": {
"eventemitter3": "^4.0.4",
"p-timeout": "^3.2.0"
},
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/p-timeout": {
"version": "3.2.0",
"resolved": "https://registry.npmmirror.com/p-timeout/-/p-timeout-3.2.0.tgz",
"integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==",
"license": "MIT",
"dependencies": {
"p-finally": "^1.0.0"
},
"engines": {
"node": ">=8"
}
},
"node_modules/path-browserify": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/path-browserify/-/path-browserify-1.0.1.tgz",
@ -1451,6 +2019,12 @@
"node": ">=0.10.0"
}
},
"node_modules/perfect-debounce": {
"version": "1.0.0",
"resolved": "https://registry.npmmirror.com/perfect-debounce/-/perfect-debounce-1.0.0.tgz",
"integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==",
"license": "MIT"
},
"node_modules/picocolors": {
"version": "1.1.1",
"resolved": "https://registry.npmmirror.com/picocolors/-/picocolors-1.1.1.tgz",
@ -1470,6 +2044,42 @@
"url": "https://github.com/sponsors/jonschlinkert"
}
},
"node_modules/pinia": {
"version": "3.0.3",
"resolved": "https://registry.npmmirror.com/pinia/-/pinia-3.0.3.tgz",
"integrity": "sha512-ttXO/InUULUXkMHpTdp9Fj4hLpD/2AoJdmAbAeW2yu1iy1k+pkFekQXw5VpC0/5p51IOR/jDaDRfRWRnMMsGOA==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^7.7.2"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"typescript": ">=4.4.4",
"vue": "^2.7.0 || ^3.5.11"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/pinia-shared-state": {
"version": "1.0.1",
"resolved": "https://registry.npmmirror.com/pinia-shared-state/-/pinia-shared-state-1.0.1.tgz",
"integrity": "sha512-S7JGPsXuV5+cp+juoVaoszdH5fEFVtagQkw4X8ivNITSN+57axloXfdECcigWq1OOWoVOjdmdanuJS8xJc+VAg==",
"license": "MIT",
"dependencies": {
"broadcast-channel": "^7.0.0"
},
"funding": {
"url": "https://github.com/sponsors/wobsoriano"
},
"peerDependencies": {
"pinia": "^3.0.0"
}
},
"node_modules/postcss": {
"version": "8.5.6",
"resolved": "https://registry.npmmirror.com/postcss/-/postcss-8.5.6.tgz",
@ -1498,6 +2108,18 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/regenerator-runtime": {
"version": "0.14.1",
"resolved": "https://registry.npmmirror.com/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"license": "MIT"
},
"node_modules/rfdc": {
"version": "1.4.1",
"resolved": "https://registry.npmmirror.com/rfdc/-/rfdc-1.4.1.tgz",
"integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==",
"license": "MIT"
},
"node_modules/rimraf": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/rimraf/-/rimraf-3.0.2.tgz",
@ -1560,6 +2182,12 @@
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
"license": "MIT"
},
"node_modules/seemly": {
"version": "0.3.10",
"resolved": "https://registry.npmmirror.com/seemly/-/seemly-0.3.10.tgz",
"integrity": "sha512-2+SMxtG1PcsL0uyhkumlOU6Qo9TAQ/WyH7tthnPIOQB05/12jz9naq6GZ6iZ6ApVsO3rr2gsnTf3++OV63kE1Q==",
"license": "MIT"
},
"node_modules/source-map-js": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/source-map-js/-/source-map-js-1.2.1.tgz",
@ -1569,6 +2197,27 @@
"node": ">=0.10.0"
}
},
"node_modules/speakingurl": {
"version": "14.0.1",
"resolved": "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz",
"integrity": "sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==",
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/superjson": {
"version": "2.2.2",
"resolved": "https://registry.npmmirror.com/superjson/-/superjson-2.2.2.tgz",
"integrity": "sha512-5JRxVqC8I8NuOUjzBbvVJAKNM8qoVuH0O77h4WInc/qC2q5IreqKxYwgkga3PfA22OayK2ikceb/B26dztPl+Q==",
"license": "MIT",
"dependencies": {
"copy-anything": "^3.0.2"
},
"engines": {
"node": ">=16"
}
},
"node_modules/tinyglobby": {
"version": "0.2.14",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.14.tgz",
@ -1592,6 +2241,12 @@
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"license": "MIT"
},
"node_modules/treemate": {
"version": "0.3.11",
"resolved": "https://registry.npmmirror.com/treemate/-/treemate-0.3.11.tgz",
"integrity": "sha512-M8RGFoKtZ8dF+iwJfAJTOH/SM4KluKOKRJpjCMhI8bG3qB74zrFoArKZ62ll0Fr3mqkMJiQOmWYkdYgDeITYQg==",
"license": "MIT"
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmmirror.com/tslib/-/tslib-2.8.1.tgz",
@ -1619,6 +2274,27 @@
"dev": true,
"license": "MIT"
},
"node_modules/unload": {
"version": "2.4.1",
"resolved": "https://registry.npmmirror.com/unload/-/unload-2.4.1.tgz",
"integrity": "sha512-IViSAm8Z3sRBYA+9wc0fLQmU9Nrxb16rcDmIiR6Y9LJSZzI7QY5QsDhqPpKOjAn0O9/kfK1TfNEMMAGPTIraPw==",
"license": "Apache-2.0",
"funding": {
"url": "https://github.com/sponsors/pubkey"
}
},
"node_modules/vdirs": {
"version": "0.1.8",
"resolved": "https://registry.npmmirror.com/vdirs/-/vdirs-0.1.8.tgz",
"integrity": "sha512-H9V1zGRLQZg9b+GdMk8MXDN2Lva0zx72MPahDKc30v+DtwKjfyOSXWRIX4t2mhDubM1H09gPhWeth/BJWPHGUw==",
"license": "MIT",
"dependencies": {
"evtd": "^0.2.2"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/vite": {
"version": "7.0.3",
"resolved": "https://registry.npmmirror.com/vite/-/vite-7.0.3.tgz",
@ -1694,6 +2370,18 @@
}
}
},
"node_modules/vooks": {
"version": "0.2.12",
"resolved": "https://registry.npmmirror.com/vooks/-/vooks-0.2.12.tgz",
"integrity": "sha512-iox0I3RZzxtKlcgYaStQYKEzWWGAduMmq+jS7OrNdQo1FgGfPMubGL3uGHOU9n97NIvfFDBGnpSvkWyb/NSn/Q==",
"license": "MIT",
"dependencies": {
"evtd": "^0.2.2"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vscode-uri": {
"version": "3.1.0",
"resolved": "https://registry.npmmirror.com/vscode-uri/-/vscode-uri-3.1.0.tgz",
@ -1722,6 +2410,53 @@
}
}
},
"node_modules/vue-i18n": {
"version": "9.14.4",
"resolved": "https://registry.npmmirror.com/vue-i18n/-/vue-i18n-9.14.4.tgz",
"integrity": "sha512-B934C8yUyWLT0EMud3DySrwSUJI7ZNiWYsEEz2gknTthqKiG4dzWE/WSa8AzCuSQzwBEv4HtG1jZDhgzPfWSKQ==",
"license": "MIT",
"dependencies": {
"@intlify/core-base": "9.14.4",
"@intlify/shared": "9.14.4",
"@vue/devtools-api": "^6.5.0"
},
"engines": {
"node": ">= 16"
},
"funding": {
"url": "https://github.com/sponsors/kazupon"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/vue-i18n/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-router": {
"version": "4.5.1",
"resolved": "https://registry.npmmirror.com/vue-router/-/vue-router-4.5.1.tgz",
"integrity": "sha512-ogAF3P97NPm8fJsE4by9dwSYtDwXIY1nFY9T6DyQnGHd1E2Da94w9JIolpe42LJGIl0DwOHBi8TcRPlPGwbTtw==",
"license": "MIT",
"dependencies": {
"@vue/devtools-api": "^6.6.4"
},
"funding": {
"url": "https://github.com/sponsors/posva"
},
"peerDependencies": {
"vue": "^3.2.0"
}
},
"node_modules/vue-router/node_modules/@vue/devtools-api": {
"version": "6.6.4",
"resolved": "https://registry.npmmirror.com/@vue/devtools-api/-/devtools-api-6.6.4.tgz",
"integrity": "sha512-sGhTPMuXqZ1rVOk32RylztWkfXTRhuS7vgAKv0zjqk8gbsHkJ7xfFf+jbySxt7tWObEJwyKaHMikV/WGDiQm8g==",
"license": "MIT"
},
"node_modules/vue-tsc": {
"version": "2.2.12",
"resolved": "https://registry.npmmirror.com/vue-tsc/-/vue-tsc-2.2.12.tgz",
@ -1739,6 +2474,24 @@
"typescript": ">=5.0.0"
}
},
"node_modules/vueuc": {
"version": "0.4.64",
"resolved": "https://registry.npmmirror.com/vueuc/-/vueuc-0.4.64.tgz",
"integrity": "sha512-wlJQj7fIwKK2pOEoOq4Aro8JdPOGpX8aWQhV8YkTW9OgWD2uj2O8ANzvSsIGjx7LTOc7QbS7sXdxHi6XvRnHPA==",
"license": "MIT",
"dependencies": {
"@css-render/vue3-ssr": "^0.15.10",
"@juggle/resize-observer": "^3.3.1",
"css-render": "^0.15.10",
"evtd": "^0.2.4",
"seemly": "^0.3.6",
"vdirs": "^0.1.4",
"vooks": "^0.2.4"
},
"peerDependencies": {
"vue": "^3.0.11"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz",

+ 7
- 1
wavecontrol-test/package.json View File

@ -13,8 +13,14 @@
"@tensorflow-models/handpose": "^0.1.0",
"@tensorflow/tfjs-backend-webgl": "^4.22.0",
"@tensorflow/tfjs-core": "^4.22.0",
"element-plus": "^2.10.4",
"fingerpose": "^0.1.0",
"vue": "^3.5.17"
"naive-ui": "^2.42.0",
"pinia": "^3.0.3",
"pinia-shared-state": "^1.0.1",
"vue": "^3.5.17",
"vue-i18n": "^9.14.4",
"vue-router": "^4.5.0"
},
"devDependencies": {
"@types/node": "^24.0.12",

+ 43
- 0
wavecontrol-test/src-py/main.py View File

@ -0,0 +1,43 @@
import os
import sys
import uvicorn
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from router.ws import router as ws_router
if hasattr(sys, 'frozen'):
os.chdir(os.path.dirname(sys.argv[0]))
app = FastAPI()
app.add_middleware(
CORSMiddleware,
allow_origins=["*"],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# 添加 WebSocket 路由
app.include_router(ws_router)
@app.get("/")
def read_root():
return "ready"
@app.get("/shutdown")
def shutdown():
import signal
import os
os.kill(os.getpid(), signal.SIGINT)
if __name__ == '__main__':
port = 62334
print(f"Starting server at http://127.0.0.1:{port}/docs")
uvicorn.run(app, host="127.0.0.1", port=port)

+ 0
- 0
wavecontrol-test/src-py/router/__init__.py View File


BIN
wavecontrol-test/src-py/router/__pycache__/__init__.cpython-312.pyc View File


BIN
wavecontrol-test/src-py/router/__pycache__/__init__.cpython-38.pyc View File


BIN
wavecontrol-test/src-py/router/__pycache__/ws.cpython-312.pyc View File


BIN
wavecontrol-test/src-py/router/__pycache__/ws.cpython-38.pyc View File


+ 225
- 0
wavecontrol-test/src-py/router/ws.py View File

@ -0,0 +1,225 @@
import json
from dataclasses import dataclass
from typing import Dict, List, Optional, Union
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
from pynput.keyboard import Controller as KeyboardController
from pynput.keyboard import Key
from pynput.mouse import Button, Controller
router = APIRouter()
# 存储活跃的WebSocket连接
active_connection: Optional[WebSocket] = None
@dataclass
class WebSocketMessage:
"""WebSocket消息数据类"""
type: str
msg: str = ""
title: str = "提示"
duration: int = 1
data: Dict = None
def __post_init__(self):
if self.data is None:
self.data = {"x": 0, "y": 0, "key_str": ""}
def to_dict(self) -> Dict:
"""将消息转换为字典格式"""
return {
"type": self.type,
"msg": self.msg,
"title": self.title,
"duration": self.duration,
"data": self.data,
}
class WebSocketMessageType:
"""WebSocket消息类型常量"""
# 系统消息类型
INFO = "info"
SUCCESS = "success"
WARNING = "warning"
ERROR = "error"
# 鼠标操作类型
MOUSE_MOVE = "mouse_move"
MOUSE_CLICK = "mouse_click"
MOUSE_SCROLL_UP = "mouse_scroll_up"
MOUSE_SCROLL_DOWN = "mouse_scroll_down"
# 键盘操作类型
SEND_KEYS = "send_keys"
class MessageSender:
"""消息发送器,发送给前端"""
@staticmethod
async def send_message(
ws_data_type: str, msg: str, title: str = "提示", duration: int = 1
) -> None:
"""
WebSocket客户端
Args:
ws_data_type:
msg:
title:
duration:
"""
if not active_connection:
return
try:
message = WebSocketMessage(
type=ws_data_type, msg=msg, title=title, duration=duration
)
await active_connection.send_json(message.to_dict())
except Exception as e:
print(f"发送消息失败: {e}")
class GestureHandler:
"""手势控制器"""
def __init__(self):
self.keyboard = KeyboardController()
self.mouse = Controller()
def move_mouse(self, x: int, y: int) -> None:
"""移动鼠标到指定位置"""
print(f"👉 正在移动鼠标到位置: ({x}, {y})")
self.mouse.position = (x, y)
def click_mouse(self) -> None:
"""点击鼠标左键"""
print("👉 正在点击鼠标")
self.mouse.click(Button.left)
def scroll_up(self) -> None:
"""向上滚动"""
self.mouse.scroll(0, 1)
def scroll_down(self) -> None:
"""向下滚动"""
self.mouse.scroll(0, -1)
def send_keys(self, key_str: str) -> None:
"""
Args:
key_str: 'ctrl+r' 'F11'
"""
try:
keys = [self.__parse_key(key) for key in key_str.split("+")]
print("👉 正在点击发送按键")
self.__execute_keys(keys)
except Exception as e:
print(f"发送按键失败: {e}")
def __parse_key(self, key_str: str) -> Union[str, Key]:
"""
Args:
key_str:
Returns:
"""
key_str = key_str.strip().lower()
if hasattr(Key, key_str):
return getattr(Key, key_str)
elif len(key_str) == 1:
return key_str
elif key_str.startswith("f"):
try:
return getattr(Key, key_str)
except AttributeError:
raise ValueError(f"无效的功能键: {key_str}")
else:
raise ValueError(f"无效的按键: {key_str}")
def __execute_keys(self, keys: List[Union[str, Key]]) -> None:
"""
Args:
keys:
"""
pressed_keys = []
try:
# 按下所有键
for key in keys:
self.keyboard.press(key)
pressed_keys.append(key)
# 释放所有键(按相反顺序)
for key in reversed(pressed_keys):
self.keyboard.release(key)
except Exception as e:
print(f"执行按键失败: {e}")
@router.websocket("/ws_wavecontrol")
async def websocket_endpoint(websocket: WebSocket):
"""WebSocket端点处理函数"""
global active_connection
await websocket.accept()
active_connection = websocket
gesture_handler = GestureHandler()
try:
while True:
data_str = await websocket.receive_text()
await _handle_message(data_str, websocket, gesture_handler)
except WebSocketDisconnect:
active_connection = None
except Exception as e:
print(f"WebSocket处理错误: {e}")
active_connection = None
async def _handle_message(
data_str: str,
websocket: WebSocket,
gesture_handler: GestureHandler,
) -> None:
"""处理WebSocket消息"""
try:
message = WebSocketMessage(**json.loads(data_str))
data = message.data
print(f"[收到指令] type={message.type}, data={data}") # 调试输出
# 处理鼠标操作
if message.type == WebSocketMessageType.MOUSE_MOVE:
gesture_handler.move_mouse(data["x"], data["y"])
elif message.type == WebSocketMessageType.MOUSE_CLICK:
gesture_handler.click_mouse()
elif message.type == WebSocketMessageType.MOUSE_SCROLL_UP:
gesture_handler.scroll_up()
elif message.type == WebSocketMessageType.MOUSE_SCROLL_DOWN:
gesture_handler.scroll_down()
# 处理键盘操作
elif message.type == WebSocketMessageType.SEND_KEYS:
gesture_handler.send_keys(data["key_str"])
except json.JSONDecodeError:
print("无效的JSON数据")
except Exception as e:
print(f"处理消息失败: {e}")

+ 41
- 24
wavecontrol-test/src/hand_landmark/VideoDetector.vue View File

@ -1,24 +1,41 @@
import { onMounted, ref } from 'vue';
import { Detector } from './hand_landmark/detector';
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
onMounted(async () => {
await detector.initialize(); //
const video = videoRef.value!;
video.width = 640;
video.height = 480;
navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
video.srcObject = stream;
video.play();
// 200ms
setInterval(async () => {
const result = await detector.detect(video);
await detector.process(result);
}, 200);
});
});
// UI +
import { createApp, ref, onMounted } from "vue";
import { Detector } from "@/hand_landmark/detector";
import { GestureHandler } from "@/hand_landmark/gesture_handler"; //
const App = {
setup() {
const videoRef = ref<HTMLVideoElement | null>(null);
const detector = new Detector();
const handler = new GestureHandler();
onMounted(async () => {
await detector.initialize();
const video = videoRef.value;
if (!video) return;
//
const stream = await navigator.mediaDevices.getUserMedia({ video: true });
video.srcObject = stream;
await video.play();
//
const loop = async () => {
const result = await detector.detect(video);
const hand = result.rightHand ?? result.leftHand;
if (hand) {
const gesture = Detector.getSingleHandGesture(hand);
handler.handleGesture(gesture, hand);
}
requestAnimationFrame(loop);
};
loop();
});
return () => <video ref={videoRef} width="640" height="480" autoplay />;
},
};
createApp(App).mount("#app");

+ 104
- 22
wavecontrol-test/src/hand_landmark/detector.ts View File

@ -1,13 +1,34 @@
import { GestureHandler } from "@/hand_landmark/gesture_handler";
import {
FilesetResolver,
GestureRecognizer,
GestureRecognizerOptions,
} from "@mediapipe/tasks-vision";
// 手势
export enum HandGesture {
// 食指举起,移动鼠标
ONLY_INDEX_UP = "only_index_up",
INDEX_AND_THUMB_UP = "index_and_thumb_up"
// 食指和拇指举起,移动鼠标
INDEX_AND_THUMB_UP = "index_and_thumb_up",
// ok手势 - 滚动屏幕
SCROLL_GESTURE = "scroll_gesture",
// 四根手指同时竖起 - enter
FOUR_FINGERS_UP = "four_fingers_up",
// 五根手指同时竖起 - 暂停/开始 识别
STOP_GESTURE = "stop_gesture",
// 6手势 - 语音识别
VOICE_GESTURE_START = "voice_gesture_start",
VOICE_GESTURE_STOP = "voice_gesture_stop",
// 其他手势
OTHER = "other",
}
interface HandLandmark {
@ -40,26 +61,25 @@ interface DetectionResult {
*/
export class Detector {
private detector: GestureRecognizer | null = null;
private gestureHandler: GestureHandler | null = null;
constructor(private modelPath = "/mediapipe/gesture_recognizer.task") {}
async initialize(useCanvas = false) {
const vision = await FilesetResolver.forVisionTasks("/mediapipe/wasm");
try {
const params: any = {
const params = {
baseOptions: {
modelAssetPath: this.modelPath,
modelAssetPath: "/mediapipe/gesture_recognizer.task",
delegate: "GPU",
},
runningMode: "VIDEO",
numHands: 1,
};
} as GestureRecognizerOptions;
if (useCanvas) {
params.canvas = document.createElement("canvas");
}
this.detector = await GestureRecognizer.createFromOptions(vision, params);
this.gestureHandler = new GestureHandler();
} catch (error: any) {
if (error.toString().includes("kGpuService")) {
await this.initialize(true);
@ -149,8 +169,26 @@ export class Detector {
const gestureMap = new Map<string, HandGesture>([
// 食指举起,移动鼠标
["0,1,0,0,0", HandGesture.ONLY_INDEX_UP],
// 鼠标左键点击手势
["1,1,0,0,0", HandGesture.INDEX_AND_THUMB_UP],
// 滚动屏幕手势 ok
["0,0,1,1,1", HandGesture.SCROLL_GESTURE],
// 四根手指同时竖起
["0,1,1,1,1", HandGesture.FOUR_FINGERS_UP],
// 五根手指同时竖起 - 暂停/开始 识别
["1,1,1,1,1", HandGesture.STOP_GESTURE],
// 6手势- 语音识别
["1,0,0,0,1", HandGesture.VOICE_GESTURE_START],
// 结束语音识别
["0,0,0,0,0", HandGesture.VOICE_GESTURE_STOP],
]);
if (gestureMap.has(fingerState)) {
@ -165,26 +203,70 @@ export class Detector {
/**
*
*/
async process(detection: DetectionResult): Promise<HandGesture> {
const hand = detection.rightHand ?? detection.leftHand;
//async process(detection: DetectionResult): Promise<HandGesture> {
// const hand = detection.rightHand ?? detection.leftHand;
if (!hand) {
console.log("没检测到手");
return HandGesture.OTHER;
}
// if (!hand) {
// console.log("没检测到手");
// return HandGesture.OTHER;
// }
const gesture = Detector.getSingleHandGesture(hand);
console.log("当前手势状态是:", gesture);
// const gesture = Detector.getSingleHandGesture(hand);
// console.log("当前手势状态是:", gesture);
if (gesture === HandGesture.ONLY_INDEX_UP) {
console.log("识别到食指竖起!");
}
// if (gesture === HandGesture.ONLY_INDEX_UP) {
// console.log("识别到食指竖起!");
// }
// if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
// console.log("识别到食指和大拇指竖起!");
// }
// if (gesture === HandGesture.THREE_FINGERS_UP) {
// console.log("识别到ok手势!");
// }
// if (gesture === HandGesture.FOUR_FINGERS_UP) {
// console.log("识别到四指竖起!");
// }
if (gesture === HandGesture.INDEX_AND_THUMB_UP) {
console.log("识别到食指和大拇指竖起!");
// if (gesture === HandGesture.STOP_GESTURE) {
// console.log("识别到五指竖起!");
// }
// if (gesture === HandGesture.VOICE_GESTURE_START) {
// console.log("识别到6手势!");
// }
// if (gesture === HandGesture.VOICE_GESTURE_STOP) {
// console.log("识别到拳头手势!");
// }
// return gesture;
async process(detection: DetectionResult): Promise<void> {
const rightHandGesture = detection.rightHand
? Detector.getSingleHandGesture(detection.rightHand)
: HandGesture.OTHER;
const leftHandGesture = detection.leftHand
? Detector.getSingleHandGesture(detection.leftHand)
: HandGesture.OTHER;
// 优先使用右手
let effectiveGesture = rightHandGesture;
if (detection.rightHand) {
effectiveGesture = rightHandGesture;
} else if (detection.leftHand) {
effectiveGesture = leftHandGesture;
}
return gesture;
// 将手势处理交给GestureHandler
if (detection.rightHand) {
this.gestureHandler?.handleGesture(effectiveGesture, detection.rightHand);
} else if (detection.leftHand) {
this.gestureHandler?.handleGesture(effectiveGesture, detection.leftHand);
}
}
}

+ 341
- 5
wavecontrol-test/src/hand_landmark/gesture_handler.ts View File

@ -1,12 +1,326 @@
// gesture_handler.ts
import { HandGesture, HandInfo } from "./detector";
import { HandGesture, HandInfo } from "@/hand_landmark/detector";
import use_app_store from "@/store/app";
import i18n from "@/locales/i18n";
// WebSocket数据类型定义
enum WsDataType {
// 系统消息类型
INFO = "info",
SUCCESS = "success",
WARNING = "warning",
ERROR = "error",
// 鼠标操作类型
MOUSE_MOVE = "mouse_move",
MOUSE_CLICK = "mouse_click",
// 键盘操作类型
SEND_KEYS = "send_keys",
// 窗口操作类型
MOUSE_SCROLL_UP = "mouse_scroll_up",
MOUSE_SCROLL_DOWN = "mouse_scroll_down",
}
interface WsData {
type: WsDataType;
msg?: string;
duration?: number;
title?: string;
data?: {
x?: number;
y?: number;
key_str?: string;
};
}
/**
* -
*
* 1. WebSocket连接
* 2.
* 3.
*/
export class TriggerAction {
private ws: WebSocket | null = null;
constructor() {
this.connectWebSocket();
}
private connectWebSocket() {
try {
this.ws = new WebSocket("ws://127.0.0.1:62334/ws_wavecontrol");
this.ws.onmessage = (event: MessageEvent) => {
const response: WsData = JSON.parse(event.data);
const app_store = use_app_store();
app_store.sub_window_info(response.msg || "");
};
this.ws.onopen = () => {
console.log("ws_wavecontrol connected");
};
this.ws.onclose = () => {
console.log("ws_wavecontrol closed, retrying...");
this.ws = null;
setTimeout(() => this.connectWebSocket(), 3000);
};
this.ws.onerror = (error) => {
console.error("ws_wavecontrol error:", error);
this.ws?.close();
};
} catch (error) {
console.error("Failed to create WebSocket instance:", error);
this.ws = null;
setTimeout(() => this.connectWebSocket(), 1000);
}
}
private send(data: { type: WsDataType } & Partial<Omit<WsData, "type">>) {
const message: WsData = {
type: data.type,
msg: data.msg || "",
title: data.title || "wavecontrol",
duration: data.duration || 1,
data: data.data || {},
};
console.log("👉 正在发送指令给后端:", message);
this.ws?.send(JSON.stringify(message));
}
moveMouse(x: number, y: number) {
this.send({
type: WsDataType.MOUSE_MOVE,
data: { x, y },
});
}
clickMouse() {
this.send({
type: WsDataType.MOUSE_CLICK,
});
}
sendKeys(key_str: string) {
this.send({
type: WsDataType.SEND_KEYS,
data: { key_str },
});
}
scrollUp() {
this.send({
type: WsDataType.MOUSE_SCROLL_UP,
});
}
scrollDown() {
this.send({
type: WsDataType.MOUSE_SCROLL_DOWN,
});
}
}
// 手势处理
export class GestureHandler {
private previousGesture: HandGesture | null = null;
private previousGestureCount = 0;
private minGestureCount = 3;
private triggerAction: TriggerAction;
private previousGesture: HandGesture | null = null; // 上一个识别到的手势
private previousGestureCount: number = 0; // 连续识别到相同手势的次数
private minGestureCount: number = 5; // 最小连续识别次数,达到后才触发操作
// 屏幕参数,用于将摄像头图像坐标映射到实际屏幕坐标
private screen_width: number = window.screen.width;
private screen_height: number = window.screen.height;
private smoothening = 7; // 平滑系数,用于减少抖动
private prev_loc_x: number = 0; // 上一帧鼠标X坐标
private prev_loc_y: number = 0; // 上一帧鼠标Y坐标
private lastClickTime: number = 0; // 上次点击时间戳(用于点击节流)
private readonly CLICK_INTERVAL = 500; // 点击最小间隔(ms)
private lastkeyTime: number = 0;
private readonly sendkeyINTERVAL = 1500; // 全屏切换间隔
private app_store: any; // 存储应用状态,比如视频宽高、边界设置等
private prev_scroll_y: number = 0;
private lastScrollTime: number = 0;
private readonly SCROLL_INTERVAL = 100; // 滚动间隔
constructor() {
this.triggerAction = new TriggerAction(); // 初始化动作触发器,用于发指令到后端
this.app_store = use_app_store(); // 获取状态管理对象
}
/**
*
*/
private handleIndexFingerUp(hand: HandInfo) {
const indexTip = this.getFingerTip(hand, 1); // 获取食指尖坐标
if (!indexTip) return;
try {
// 将 landmark 坐标转为视频画面坐标(x 轴方向翻转)
const video_x =
this.app_store.VIDEO_WIDTH - indexTip.x * this.app_store.VIDEO_WIDTH;
const video_y = indexTip.y * this.app_store.VIDEO_HEIGHT;
/**
*
*/
function mapRange(
value: number,
fromMin: number,
fromMax: number,
toMin: number,
toMax: number
): number {
return (
((value - fromMin) * (toMax - toMin)) / (fromMax - fromMin) + toMin
);
}
// 将视频坐标映射为屏幕坐标
let screenX = mapRange(
video_x,
this.app_store.config.boundary_left,
this.app_store.config.boundary_left + this.app_store.config.boundary_width,
0,
this.screen_width
);
let screenY = mapRange(
video_y,
this.app_store.config.boundary_top,
this.app_store.config.boundary_top + this.app_store.config.boundary_height,
0,
this.screen_height
);
// 加入平滑处理,缓解抖动
screenX = this.prev_loc_x + (screenX - this.prev_loc_x) / this.smoothening;
screenY = this.prev_loc_y + (screenY - this.prev_loc_y) / this.smoothening;
// 更新上一帧坐标
this.prev_loc_x = screenX;
this.prev_loc_y = screenY;
// 更新状态,触发鼠标移动指令
this.app_store.sub_windows.x = screenX + 10;
this.app_store.sub_windows.y = screenY;
this.triggerAction.moveMouse(screenX, screenY);
} catch (error) {
console.error("处理鼠标移动失败:", error);
}
}
/**
* +
*/
private handleMouseClick() {
const now = Date.now();
if (now - this.lastClickTime < this.CLICK_INTERVAL) {
return; // 距离上次点击时间太短,忽略(节流)
}
this.lastClickTime = now;
// 发送鼠标点击指令
this.triggerAction.clickMouse();
}
/**
* -
*/
private handleFourFingers() {
try {
const key_str = this.app_store.config.four_fingers_up_send || "f";
const now = Date.now();
if (now - this.lastkeyTime < this.sendkeyINTERVAL) {
return;
}
this.lastkeyTime = now;
this.triggerAction.sendKeys(key_str);
} catch (error) {
console.error("处理四指手势失败:", error);
}
}
// 拇指和食指捏合,滚动屏幕
private handlescroll(hand: HandInfo) {
const indexTip = this.getFingerTip(hand, 1);
const thumbTip = this.getFingerTip(hand, 0);
if (!indexTip || !thumbTip) {
this.prev_scroll_y = 0;
return;
}
const now = Date.now();
if (now - this.lastScrollTime < this.SCROLL_INTERVAL) {
return;
}
this.lastScrollTime = now;
// 计算食指和拇指的距离
const distance = Math.sqrt(
(indexTip.x - thumbTip.x) ** 2 + (indexTip.y - thumbTip.y) ** 2
);
console.log(i18n.global.t("当前食指和拇指距离"), distance);
// 如果距离大于阈值,说明没有捏合,重置上一次的 Y 坐标
if (
distance >
this.app_store.config.scroll_gesture_2_thumb_and_index_threshold
) {
this.prev_scroll_y = 0;
return;
}
// 如果是第一次检测到捏合,记录当前 Y 坐标
if (this.prev_scroll_y === 0) {
this.prev_scroll_y = indexTip.y;
return;
}
// 计算 Y
const deltaY = indexTip.y - this.prev_scroll_y;
// 如果变化超过阈值,则触发滚动
if (Math.abs(deltaY) > 0.008) {
if (deltaY < 0) {
// 手指向上移动,向上滚动
this.triggerAction.scrollUp();
} else {
// 手指向下移动,向下滚动
this.triggerAction.scrollDown();
}
// 更新上一次的 Y 坐标
this.prev_scroll_y = indexTip.y;
}
}
/**
* landmark
* fingerIndex: 0=, 1=, 2=, ...
*/
private getFingerTip(hand: HandInfo, fingerIndex: number) {
if (!hand) return null;
const tipIndices = [4, 8, 12, 16, 20]; // 手指指尖 landmark 的索引
return hand.landmarks[tipIndices[fingerIndex]];
}
/**
*
*/
handleGesture(gesture: HandGesture, hand: HandInfo) {
// 判断手势是否重复,连续计数
if (gesture === this.previousGesture) {
this.previousGestureCount++;
} else {
@ -14,8 +328,30 @@ export class GestureHandler {
this.previousGestureCount = 1;
}
// 鼠标移动(不需要连续确认)
if (gesture === HandGesture.ONLY_INDEX_UP) {
this.handleIndexFingerUp(hand);
return;
}
// 连续识别足够多次 → 触发操作
if (this.previousGestureCount >= this.minGestureCount) {
console.log("识别手势类型:", gesture);
switch (gesture) {
case HandGesture.INDEX_AND_THUMB_UP: // 自定义为点击手势
this.handleMouseClick();
break;
case HandGesture.FOUR_FINGERS_UP:
this.handleFourFingers();
break;
case HandGesture.SCROLL_GESTURE:
this.handlescroll(hand);
break;
}
}
}
}

+ 49
- 0
wavecontrol-test/src/locales/en.ts View File

@ -0,0 +1,49 @@
export default {
"手势识别控制": "Gesture Recognition Control",
"运行中": "Running",
"已停止": "Stopped",
"开机自启动": "Auto Start",
"显示识别窗口": "Show Recognition Window",
"摄像头选择": "Camera Selection",
"手势操作指南": "Gesture Guide",
"光标控制": "Cursor Control",
"竖起食指滑动控制光标位置": "Slide with index finger to control cursor position",
"单击操作": "Click Operation",
"双指举起执行鼠标单击": "Raise two fingers to perform mouse click",
"Rock手势执行鼠标单击": "Rock gesture to perform mouse click",
"滚动控制": "Scroll Control",
"三指上下滑动控制页面滚动": "Slide three fingers up/down to control page scrolling",
"(okay手势)食指和拇指捏合滚动页面": "(okay gesture)Pinch with index and thumb to scroll page",
"食指和拇指距离小于": "Index and thumb distance less than",
"触发捏合": "Trigger pinch",
"默认值0.02": "Default value 0.02",
"可以通过右键->检查->控制台->捏合手势->查看当前距离": "Can check current distance by right-click -> inspect -> console -> pinch gesture",
"全屏控制": "Full Screen Control",
"四指并拢发送按键": "Four fingers together to send key",
"点击设置快捷键": "Click to set shortcut",
"请按下按键...": "Please press keys...",
"点击设置": "Click to set",
"退格": "Backspace",
"发送退格键": "Send backspace key",
"开始语音识别": "Start Voice Recognition",
"六指手势开始语音识别": "Six fingers gesture to start voice recognition",
"结束语音识别": "End Voice Recognition",
"拳头手势结束语音识别": "Fist gesture to end voice recognition",
"暂停/继续": "Pause/Resume",
"单手张开1.5秒 暂停/继续 手势识别": "Open one hand for 1.5 seconds to pause/resume gesture recognition",
"识别框x": "Recognition box x",
"识别框y": "Recognition box y",
"识别框宽": "Recognition box width",
"识别框高": "Recognition box height",
// 通知
"WaveControl": "WaveControl",
"提示": "Tip",
"停止语音识别": "Stop Voice Recognition",
"手势识别": "Gesture Recognition",
"继续手势识别": "Continue Gesture Recognition",
"暂停手势识别": "Pause Gesture Recognition",
};

+ 18
- 0
wavecontrol-test/src/locales/i18n.ts View File

@ -0,0 +1,18 @@
import { createI18n } from "vue-i18n";
// 导入语言包
import en from "./en";
import zh from "./zh";
// 创建 i18n 实例
const i18n = createI18n({
locale: navigator.language.split("-")[0], // 使用系统语言作为默认语言
// locale: "en", // 使用系统语言作为默认语言
fallbackLocale: "zh", // 回退语言
messages: {
en,
zh,
},
});
export default i18n;

+ 47
- 0
wavecontrol-test/src/locales/zh.ts View File

@ -0,0 +1,47 @@
export default {
"手势识别控制": "手势识别控制",
"运行中": "运行中",
"已停止": "已停止",
"显示识别窗口": "显示识别窗口",
"摄像头选择": "摄像头选择",
"手势操作指南": "手势操作指南",
"开机自启动": "开机自启动",
"光标控制": "光标控制",
"竖起食指滑动控制光标位置": "竖起食指滑动控制光标位置",
"单击操作": "单击操作",
"双指举起执行鼠标单击": "双指举起执行鼠标单击",
"Rock手势执行鼠标单击": "Rock手势执行鼠标单击",
"滚动控制": "滚动控制",
"三指上下滑动控制页面滚动": "三指上下滑动控制页面滚动",
"(okay手势)食指和拇指捏合滚动页面": "(okay手势)食指和拇指捏合滚动页面",
"食指和拇指距离小于": "食指和拇指距离小于",
"触发捏合": "触发捏合",
"全屏控制": "全屏控制",
"四指并拢发送按键": "四指并拢发送按键",
"点击设置快捷键": "点击设置快捷键",
"请按下按键...": "请按下按键...",
"点击设置": "点击设置",
"退格": "退格",
"发送退格键": "发送退格键",
"开始语音识别": "开始语音识别",
"六指手势开始语音识别": "六指手势开始语音识别",
"结束语音识别": "结束语音识别",
"拳头手势结束语音识别": "拳头手势结束语音识别",
"暂停/继续": "暂停/继续",
"单手张开1.5秒 暂停/继续 手势识别": "单手张开1.5秒 暂停/继续 手势识别",
"识别框x": "识别框x",
"识别框y": "识别框y",
"识别框宽": "识别框宽",
"识别框高": "识别框高",
"默认值0.02": "默认值0.02",
"可以通过右键->检查->控制台->捏合手势->查看当前距离": "可以通过右键->检查->控制台->捏合手势->查看当前距离",
// 通知
"WaveControl": "WaveControl",
"提示": "提示",
"停止语音识别": "停止语音识别",
"手势识别": "手势识别",
"继续手势识别": "继续手势识别",
"暂停手势识别": "暂停手势识别",
};

+ 10
- 4
wavecontrol-test/src/main.ts View File

@ -1,5 +1,11 @@
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
import { createApp } from "vue";
import App from "@/App.vue";
createApp(App).mount('#app')
import { createPinia } from "pinia";
const app = createApp(App);
const pinia = createPinia();
app.use(pinia);
app.mount("#app");

+ 87
- 0
wavecontrol-test/src/store/app.ts View File

@ -0,0 +1,87 @@
import { defineStore } from "pinia";
interface Camera {
deviceId: string;
label: string;
kind: string;
}
enum NotiType {
INFO = "info",
SUCCESS = "success",
WARNING = "warning",
ERROR = "error",
}
export const use_app_store = defineStore("app-store", {
state: () => ({
config: {
auto_start: false,
show_window: false,
four_fingers_up_send: "enter",
selected_camera_id: "",
// 识别框
boundary_left: 100,
boundary_top: 100,
boundary_width: 100,
boundary_height: 100,
// 手势识别
scroll_gesture_2_thumb_and_index_threshold: 0.02, // 食指和拇指距离阈值
},
sub_windows: {
x: 0,
y: 0,
progress: 0,
notification: "",
noti_type: NotiType.INFO,
},
mission_running: false,
cameras: [] as Camera[],
VIDEO_WIDTH: 640,
VIDEO_HEIGHT: 480,
flag_detecting: false,
}),
// PiniaSharedState 来共享不同 tauri 窗口之间的状态
share: {
// Override global config for this store.
enable: true,
initialize: true,
},
actions: {
is_macos() {
return navigator.userAgent.includes("Mac");
},
is_windows() {
return navigator.userAgent.includes("Windows");
},
is_linux() {
return navigator.userAgent.includes("Linux");
},
async sub_window_info(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.INFO;
},
async sub_window_success(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.SUCCESS;
},
async sub_window_warning(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.WARNING;
},
async sub_window_error(body: string) {
this.sub_windows.notification = body;
this.sub_windows.noti_type = NotiType.ERROR;
},
},
});
export default use_app_store;

+ 7
- 0
wavecontrol-test/src/vite-env.d copy.ts View File

@ -0,0 +1,7 @@
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";
const component: DefineComponent<{}, {}, any>;
export default component;
}

+ 9
- 0
wavecontrol-test/test_mouse.py View File

@ -0,0 +1,9 @@
from pynput.mouse import Controller
import time
mouse = Controller()
print("3秒后移动鼠标到位置 (300, 300)...")
time.sleep(3)
mouse.position = (300, 300)
print("鼠标已移动 ✅")

+ 0
- 15
wavecontrol-test/tsconfig.app.json View File

@ -1,15 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.dom.json",
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

+ 28
- 5
wavecontrol-test/tsconfig.json View File

@ -1,7 +1,30 @@
{
"files": [],
"references": [
{ "path": "./tsconfig.app.json" },
{ "path": "./tsconfig.node.json" }
]
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
/* Bundler mode */
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"resolveJsonModule": true,
"isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"] // @ src
}
},
"include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}

+ 3
- 18
wavecontrol-test/tsconfig.node.json View File

@ -1,25 +1,10 @@
{
"compilerOptions": {
"tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo",
"target": "ES2023",
"lib": ["ES2023"],
"module": "ESNext",
"composite": true,
"skipLibCheck": true,
/* Bundler mode */
"module": "ESNext",
"moduleResolution": "bundler",
"allowImportingTsExtensions": true,
"verbatimModuleSyntax": true,
"moduleDetection": "force",
"noEmit": true,
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"erasableSyntaxOnly": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedSideEffectImports": true
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

Loading…
Cancel
Save