├── .github
└── workflows
│ └── static.yml
├── .gitignore
├── .npmignore
├── LICENSE
├── README.md
├── assets
├── box-recording.gif
├── gesture-table.png
├── sky.jpg
├── sparrow-recording.gif
├── sparrow
│ ├── license.txt
│ ├── scene.bin
│ ├── scene.gltf
│ └── textures
│ │ └── M_Sparrow_baseColor.png
└── title.png
├── data
├── category.csv
└── label.csv
├── example
├── basic-example-2.html
├── basic-example.html
└── self-training-model.html
├── model
├── model.json
└── model.weights.bin
├── old-model
├── model.json
└── model.weights.bin
├── package-lock.json
├── package.json
├── src
├── aframe.js
└── gesture.js
├── vite.config.js
└── website
├── assets
├── floor.jpg
└── gesture-example-table.png
├── gesture-recognition-playground
└── ascene.html
├── index.html
├── styles.css
└── train-model
├── label.html
└── label.js
/.github/workflows/static.yml:
--------------------------------------------------------------------------------
1 | # Simple workflow for deploying static content to GitHub Pages
2 | name: Deploy static content to Pages
3 |
4 | on:
5 | # Runs on pushes targeting the default branch
6 | push:
7 | branches: ["main"]
8 |
9 | # Allows you to run this workflow manually from the Actions tab
10 | workflow_dispatch:
11 |
12 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
13 | permissions:
14 | contents: read
15 | pages: write
16 | id-token: write
17 |
18 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
19 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
20 | concurrency:
21 | group: "pages"
22 | cancel-in-progress: false
23 |
24 | jobs:
25 | # Single deploy job since we're just deploying
26 | deploy:
27 | environment:
28 | name: github-pages
29 | url: ${{ steps.deployment.outputs.page_url }}
30 | runs-on: ubuntu-latest
31 | steps:
32 | - name: Checkout
33 | uses: actions/checkout@v4
34 | - name: Setup Pages
35 | uses: actions/configure-pages@v4
36 | - name: Upload artifact
37 | uses: actions/upload-pages-artifact@v3
38 | with:
39 | # Upload entire repository
40 | path: './website'
41 | - name: Deploy to GitHub Pages
42 | id: deployment
43 | uses: actions/deploy-pages@v4
44 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules/
2 | old-model/
3 | dist/
4 | *.DS_Store
--------------------------------------------------------------------------------
/.npmignore:
--------------------------------------------------------------------------------
1 | example/
2 | website/
3 | data/
4 | old-model/
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Rylan Peng
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Web Augmented Reality. A-Frame Component. Currently supports gesture recognition
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 | ## **Feature**
24 |
25 | :pushpin: **Gesture Recognition A-Frame Component**: Capable of recognizing 18 gestures.
26 |
27 | :pushpin: **Custom Gesture Training**: Train personalized models and pass them to the FernAR engine for customized gesture recognition.
28 |
29 | ## **Gesture Recognition**:
30 | Place the library into a scene to initiate an AR scene using the device camera. This allows you to use gestures for controlling A-Frame elements. If the library's predefined gestures are insufficient, you can explore the [Train Your Own Model](#train-your-own-model) section to train a custom model.
31 |
32 | ## **Simple Example**:
33 |
34 | Live demo: [Enter AR](https://rylanpeng.github.io/fern-ar-js/gesture-recognition-playground/ascene.html)
35 |
36 | ```html
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
56 |
57 |
58 |
59 | ```
60 |
61 | ### **Step 1: Add fern-ar library**
62 |
63 | #### CDN
64 |
65 | ```html
66 |
67 | ```
68 |
69 | #### NPM
70 | ```cmd
71 | $ npm install fern-ar@latest
72 | ```
73 |
74 | ### **Step 2: Add `fernar-gesture` to ``**
75 |
76 | ```css
77 |
78 | ```
79 |
80 | #### Note 1: For using self-training ML model, utilize the `FERNAR.updateModel` function with JSON (string), weights (array), and weight bin file name.
81 |
82 | - Refer to the example [self-training-model](./example/self-training-model.html) for more details.
83 |
84 | ```js
85 | async function initializeScene() {
86 | const jsonModelPath = "../test-model/model.json";
87 | const binModelPath = "../test-model/model.weights.bin";
88 |
89 | // Fetching JSON model file
90 | const jsonResponse = await fetch(jsonModelPath);
91 | const jsonModelFile = await jsonResponse.text();
92 |
93 | // Fetching binary model file
94 | const binResponse = await fetch(binModelPath);
95 | const binModelFile = new Uint8Array(await binResponse.arrayBuffer());
96 |
97 | await FERNAR.updateModel(jsonModelFile, binModelFile, binModelPath);
98 | }
99 | initializeScene();
100 | ```
101 |
102 | #### Note 2: Use `FERNAR.setDebugMode` to enable debug messages
103 |
104 | ```js
105 | FERNAR.setDebugMode(true);
106 | ```
107 |
108 | #### Note 3: Properties for `fernar-gesture`
109 |
110 | | Property | Type | default |
111 | | -------- | -------- | -------- |
112 | | drawLandmarker | boolean | true |
113 | | threshold | int | 10 |
114 | | confidence | number | 0.7 |
115 | | planePosition | array | -6 3 -7 |
116 | | planeWidth | int | 5 |
117 | | planeHeight | int | 5 |
118 |
119 | ### **Step 3: Add a camera to ``**
120 |
121 | ```html
122 |
123 |
124 |
125 | ```
126 |
127 | ### **Step 4: Register entities for gesture events**
128 |
129 | Instruct FernAR to register the entity for gesture events by adding `fernar-gesture-target` to the entity and specifying an array of gesture IDs the entity should listen to. The event name would be `fernar-gesture-event-${gesture-id}`.
130 |
131 | For example, by using ` `, the FernAR engine will send **fernar-gesture-event-1**, **fernar-gesture-event-2**, and **fernar-gesture-event-5** events to the `` every time the engine detects the corresponding gesture.
132 |
133 | ### **Step 4: Listen to the event from FernAR engine and perform desired actions**
134 |
135 | ```js
136 | entity.addEventListener("fernar-gesture-event-0", function (event) {
137 | ...
138 | });
139 | ```
140 |
141 | ## **Supported Gestures**:
142 |
143 |
144 |
145 |
146 | ## **Train Your Own Model**:
147 |
148 | Refer to this Git page [Train Model](https://rylanpeng.github.io/fern-ar-js/train-model/label.html) to train a customized model. Provide a CSV file (you can use the one in **data/label.csv**), enter the new gesture ID you want to recognize, and perferm the gesture in front of the camera. The web pages will record the gesture landmark every time you click `Store Gesture`.
149 |
150 | After labeling, you can either click the `Download CSV File` button to download the new labeling CSV file or specify the total number of gestures you want to train (ex: gestures 0,1,2,3 would be total 4 gestures) and click `Start Training` to train the model using the CSV file you just labeled.
151 |
152 | Once the training is complete, the web pages will automatically save the model in your device's Download folder. You should find two files: model.json and model.weight.bin.
153 |
154 | ## **Contributing**
155 |
156 | Any support or questions are welcome! If you encounter any bugs or have suggestions for improvements, please feel free to open issues, and let's discuss them together.
157 |
158 | ## **Reference**:
159 | :star: The gesture recognition heavily relies on the repo [hand-gesture-recognition-using-mediapipe](https://github.com/Kazuhito00/hand-gesture-recognition-using-mediapipe)
160 |
161 | :star: A-Frame: https://github.com/aframevr/aframe
162 |
163 | :star: MediaPipe: https://developers.google.com/mediapipe/solutions/vision/hand_landmarker
164 |
165 | :star: Mind AR: https://github.com/hiukim/mind-ar-js
166 |
--------------------------------------------------------------------------------
/assets/box-recording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/assets/box-recording.gif
--------------------------------------------------------------------------------
/assets/gesture-table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/assets/gesture-table.png
--------------------------------------------------------------------------------
/assets/sky.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/assets/sky.jpg
--------------------------------------------------------------------------------
/assets/sparrow-recording.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/assets/sparrow-recording.gif
--------------------------------------------------------------------------------
/assets/sparrow/license.txt:
--------------------------------------------------------------------------------
1 | Model Information:
2 | * title: Sparrow - Quirky Series
3 | * source: https://sketchfab.com/3d-models/sparrow-quirky-series-289e7db66cfa45fbbe7624b8f48f6c8c
4 | * author: Omabuarts Studio (https://sketchfab.com/omabuarts)
5 |
6 | Model License:
7 | * license type: CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
8 | * requirements: Author must be credited. Commercial use is allowed.
9 |
10 | If you use this 3D model in your project be sure to copy paste this credit wherever you share it:
11 | This work is based on "Sparrow - Quirky Series" (https://sketchfab.com/3d-models/sparrow-quirky-series-289e7db66cfa45fbbe7624b8f48f6c8c) by Omabuarts Studio (https://sketchfab.com/omabuarts) licensed under CC-BY-4.0 (http://creativecommons.org/licenses/by/4.0/)
--------------------------------------------------------------------------------
/assets/sparrow/scene.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/assets/sparrow/scene.bin
--------------------------------------------------------------------------------
/assets/sparrow/textures/M_Sparrow_baseColor.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/assets/sparrow/textures/M_Sparrow_baseColor.png
--------------------------------------------------------------------------------
/assets/title.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/assets/title.png
--------------------------------------------------------------------------------
/data/category.csv:
--------------------------------------------------------------------------------
1 | 0 Paper
2 | 1 Rock
3 | 2 Pointer
4 | 3 Two
5 | 4 Three
6 | 5 Thumb up
7 | 6 OK
8 | 7 Thumb down
9 | 8 Rock
10 | 9 Four
11 | 10 Six
12 | 11 Seven
13 | 12 Eight
14 | 13 Nine
15 | 14 Double fingers
16 | 15 Pinky
17 | 16 Spiderman
18 | 17 Big O
19 |
--------------------------------------------------------------------------------
/example/basic-example-2.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
37 |
38 |
42 |
43 |
44 |
45 |
46 |
47 |
54 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/example/basic-example.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
28 |
29 |
30 |
31 |
--------------------------------------------------------------------------------
/example/self-training-model.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
30 |
31 |
32 |
41 |
42 |
43 |
44 |
--------------------------------------------------------------------------------
/model/model.json:
--------------------------------------------------------------------------------
1 | {"modelTopology":{"class_name":"Sequential","config":{"name":"sequential_2","layers":[{"class_name":"Dense","config":{"units":20,"activation":"relu","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense4","trainable":true,"batch_input_shape":[null,42],"dtype":"float32"}},{"class_name":"Dropout","config":{"rate":null,"noise_shape":null,"seed":null,"name":"dropout_Dropout3","trainable":true}},{"class_name":"Dense","config":{"units":10,"activation":"relu","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense5","trainable":true}},{"class_name":"Dropout","config":{"rate":null,"noise_shape":null,"seed":null,"name":"dropout_Dropout4","trainable":true}},{"class_name":"Dense","config":{"units":18,"activation":"softmax","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense6","trainable":true}}]},"keras_version":"tfjs-layers 4.17.0","backend":"tensor_flow.js"},"format":"layers-model","generatedBy":"TensorFlow.js tfjs-layers v4.17.0","convertedBy":null,"weightsManifest":[{"paths":["./model.weights.bin"],"weights":[{"name":"dense_Dense4/kernel","shape":[42,20],"dtype":"float32"},{"name":"dense_Dense4/bias","shape":[20],"dtype":"float32"},{"name":"dense_Dense5/kernel","shape":[20,10],"dtype":"float32"},{"name":"dense_Dense5/bias","shape":[10],"dtype":"float32"},{"name":"dense_Dense6/kernel","shape":[10,18],"dtype":"float32"},{"name":"dense_Dense6/bias","shape":[18],"dtype":"float32"}]}]}
--------------------------------------------------------------------------------
/model/model.weights.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/model/model.weights.bin
--------------------------------------------------------------------------------
/old-model/model.json:
--------------------------------------------------------------------------------
1 | {"modelTopology":{"class_name":"Sequential","config":{"name":"sequential_1","layers":[{"class_name":"Dense","config":{"units":20,"activation":"relu","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense1","trainable":true,"batch_input_shape":[null,42],"dtype":"float32"}},{"class_name":"Dropout","config":{"rate":null,"noise_shape":null,"seed":null,"name":"dropout_Dropout1","trainable":true}},{"class_name":"Dense","config":{"units":10,"activation":"relu","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense2","trainable":true}},{"class_name":"Dropout","config":{"rate":null,"noise_shape":null,"seed":null,"name":"dropout_Dropout2","trainable":true}},{"class_name":"Dense","config":{"units":18,"activation":"softmax","use_bias":true,"kernel_initializer":{"class_name":"VarianceScaling","config":{"scale":1,"mode":"fan_avg","distribution":"normal","seed":null}},"bias_initializer":{"class_name":"Zeros","config":{}},"kernel_regularizer":null,"bias_regularizer":null,"activity_regularizer":null,"kernel_constraint":null,"bias_constraint":null,"name":"dense_Dense3","trainable":true}}]},"keras_version":"tfjs-layers 4.17.0","backend":"tensor_flow.js"},"format":"layers-model","generatedBy":"TensorFlow.js tfjs-layers v4.17.0","convertedBy":null,"weightsManifest":[{"paths":["./model.weights.bin"],"weights":[{"name":"dense_Dense1/kernel","shape":[42,20],"dtype":"float32"},{"name":"dense_Dense1/bias","shape":[20],"dtype":"float32"},{"name":"dense_Dense2/kernel","shape":[20,10],"dtype":"float32"},{"name":"dense_Dense2/bias","shape":[10],"dtype":"float32"},{"name":"dense_Dense3/kernel","shape":[10,18],"dtype":"float32"},{"name":"dense_Dense3/bias","shape":[18],"dtype":"float32"}]}]}
--------------------------------------------------------------------------------
/old-model/model.weights.bin:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/old-model/model.weights.bin
--------------------------------------------------------------------------------
/package-lock.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fern-ar",
3 | "version": "0.0.23",
4 | "lockfileVersion": 3,
5 | "requires": true,
6 | "packages": {
7 | "": {
8 | "name": "fern-ar",
9 | "version": "0.0.23",
10 | "license": "MIT",
11 | "dependencies": {
12 | "@mediapipe/tasks-vision": "^0.10.12",
13 | "@tensorflow/tfjs": "^4.17.0"
14 | },
15 | "devDependencies": {
16 | "vite": "^5.1.5"
17 | }
18 | },
19 | "node_modules/@esbuild/aix-ppc64": {
20 | "version": "0.19.12",
21 | "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.19.12.tgz",
22 | "integrity": "sha512-bmoCYyWdEL3wDQIVbcyzRyeKLgk2WtWLTWz1ZIAZF/EGbNOwSA6ew3PftJ1PqMiOOGu0OyFMzG53L0zqIpPeNA==",
23 | "cpu": [
24 | "ppc64"
25 | ],
26 | "dev": true,
27 | "optional": true,
28 | "os": [
29 | "aix"
30 | ],
31 | "engines": {
32 | "node": ">=12"
33 | }
34 | },
35 | "node_modules/@esbuild/android-arm": {
36 | "version": "0.19.12",
37 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.19.12.tgz",
38 | "integrity": "sha512-qg/Lj1mu3CdQlDEEiWrlC4eaPZ1KztwGJ9B6J+/6G+/4ewxJg7gqj8eVYWvao1bXrqGiW2rsBZFSX3q2lcW05w==",
39 | "cpu": [
40 | "arm"
41 | ],
42 | "dev": true,
43 | "optional": true,
44 | "os": [
45 | "android"
46 | ],
47 | "engines": {
48 | "node": ">=12"
49 | }
50 | },
51 | "node_modules/@esbuild/android-arm64": {
52 | "version": "0.19.12",
53 | "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.19.12.tgz",
54 | "integrity": "sha512-P0UVNGIienjZv3f5zq0DP3Nt2IE/3plFzuaS96vihvD0Hd6H/q4WXUGpCxD/E8YrSXfNyRPbpTq+T8ZQioSuPA==",
55 | "cpu": [
56 | "arm64"
57 | ],
58 | "dev": true,
59 | "optional": true,
60 | "os": [
61 | "android"
62 | ],
63 | "engines": {
64 | "node": ">=12"
65 | }
66 | },
67 | "node_modules/@esbuild/android-x64": {
68 | "version": "0.19.12",
69 | "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.19.12.tgz",
70 | "integrity": "sha512-3k7ZoUW6Q6YqhdhIaq/WZ7HwBpnFBlW905Fa4s4qWJyiNOgT1dOqDiVAQFwBH7gBRZr17gLrlFCRzF6jFh7Kew==",
71 | "cpu": [
72 | "x64"
73 | ],
74 | "dev": true,
75 | "optional": true,
76 | "os": [
77 | "android"
78 | ],
79 | "engines": {
80 | "node": ">=12"
81 | }
82 | },
83 | "node_modules/@esbuild/darwin-arm64": {
84 | "version": "0.19.12",
85 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.19.12.tgz",
86 | "integrity": "sha512-B6IeSgZgtEzGC42jsI+YYu9Z3HKRxp8ZT3cqhvliEHovq8HSX2YX8lNocDn79gCKJXOSaEot9MVYky7AKjCs8g==",
87 | "cpu": [
88 | "arm64"
89 | ],
90 | "dev": true,
91 | "optional": true,
92 | "os": [
93 | "darwin"
94 | ],
95 | "engines": {
96 | "node": ">=12"
97 | }
98 | },
99 | "node_modules/@esbuild/darwin-x64": {
100 | "version": "0.19.12",
101 | "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.19.12.tgz",
102 | "integrity": "sha512-hKoVkKzFiToTgn+41qGhsUJXFlIjxI/jSYeZf3ugemDYZldIXIxhvwN6erJGlX4t5h417iFuheZ7l+YVn05N3A==",
103 | "cpu": [
104 | "x64"
105 | ],
106 | "dev": true,
107 | "optional": true,
108 | "os": [
109 | "darwin"
110 | ],
111 | "engines": {
112 | "node": ">=12"
113 | }
114 | },
115 | "node_modules/@esbuild/freebsd-arm64": {
116 | "version": "0.19.12",
117 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.12.tgz",
118 | "integrity": "sha512-4aRvFIXmwAcDBw9AueDQ2YnGmz5L6obe5kmPT8Vd+/+x/JMVKCgdcRwH6APrbpNXsPz+K653Qg8HB/oXvXVukA==",
119 | "cpu": [
120 | "arm64"
121 | ],
122 | "dev": true,
123 | "optional": true,
124 | "os": [
125 | "freebsd"
126 | ],
127 | "engines": {
128 | "node": ">=12"
129 | }
130 | },
131 | "node_modules/@esbuild/freebsd-x64": {
132 | "version": "0.19.12",
133 | "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.19.12.tgz",
134 | "integrity": "sha512-EYoXZ4d8xtBoVN7CEwWY2IN4ho76xjYXqSXMNccFSx2lgqOG/1TBPW0yPx1bJZk94qu3tX0fycJeeQsKovA8gg==",
135 | "cpu": [
136 | "x64"
137 | ],
138 | "dev": true,
139 | "optional": true,
140 | "os": [
141 | "freebsd"
142 | ],
143 | "engines": {
144 | "node": ">=12"
145 | }
146 | },
147 | "node_modules/@esbuild/linux-arm": {
148 | "version": "0.19.12",
149 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.19.12.tgz",
150 | "integrity": "sha512-J5jPms//KhSNv+LO1S1TX1UWp1ucM6N6XuL6ITdKWElCu8wXP72l9MM0zDTzzeikVyqFE6U8YAV9/tFyj0ti+w==",
151 | "cpu": [
152 | "arm"
153 | ],
154 | "dev": true,
155 | "optional": true,
156 | "os": [
157 | "linux"
158 | ],
159 | "engines": {
160 | "node": ">=12"
161 | }
162 | },
163 | "node_modules/@esbuild/linux-arm64": {
164 | "version": "0.19.12",
165 | "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.19.12.tgz",
166 | "integrity": "sha512-EoTjyYyLuVPfdPLsGVVVC8a0p1BFFvtpQDB/YLEhaXyf/5bczaGeN15QkR+O4S5LeJ92Tqotve7i1jn35qwvdA==",
167 | "cpu": [
168 | "arm64"
169 | ],
170 | "dev": true,
171 | "optional": true,
172 | "os": [
173 | "linux"
174 | ],
175 | "engines": {
176 | "node": ">=12"
177 | }
178 | },
179 | "node_modules/@esbuild/linux-ia32": {
180 | "version": "0.19.12",
181 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.19.12.tgz",
182 | "integrity": "sha512-Thsa42rrP1+UIGaWz47uydHSBOgTUnwBwNq59khgIwktK6x60Hivfbux9iNR0eHCHzOLjLMLfUMLCypBkZXMHA==",
183 | "cpu": [
184 | "ia32"
185 | ],
186 | "dev": true,
187 | "optional": true,
188 | "os": [
189 | "linux"
190 | ],
191 | "engines": {
192 | "node": ">=12"
193 | }
194 | },
195 | "node_modules/@esbuild/linux-loong64": {
196 | "version": "0.19.12",
197 | "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.19.12.tgz",
198 | "integrity": "sha512-LiXdXA0s3IqRRjm6rV6XaWATScKAXjI4R4LoDlvO7+yQqFdlr1Bax62sRwkVvRIrwXxvtYEHHI4dm50jAXkuAA==",
199 | "cpu": [
200 | "loong64"
201 | ],
202 | "dev": true,
203 | "optional": true,
204 | "os": [
205 | "linux"
206 | ],
207 | "engines": {
208 | "node": ">=12"
209 | }
210 | },
211 | "node_modules/@esbuild/linux-mips64el": {
212 | "version": "0.19.12",
213 | "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.19.12.tgz",
214 | "integrity": "sha512-fEnAuj5VGTanfJ07ff0gOA6IPsvrVHLVb6Lyd1g2/ed67oU1eFzL0r9WL7ZzscD+/N6i3dWumGE1Un4f7Amf+w==",
215 | "cpu": [
216 | "mips64el"
217 | ],
218 | "dev": true,
219 | "optional": true,
220 | "os": [
221 | "linux"
222 | ],
223 | "engines": {
224 | "node": ">=12"
225 | }
226 | },
227 | "node_modules/@esbuild/linux-ppc64": {
228 | "version": "0.19.12",
229 | "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.19.12.tgz",
230 | "integrity": "sha512-nYJA2/QPimDQOh1rKWedNOe3Gfc8PabU7HT3iXWtNUbRzXS9+vgB0Fjaqr//XNbd82mCxHzik2qotuI89cfixg==",
231 | "cpu": [
232 | "ppc64"
233 | ],
234 | "dev": true,
235 | "optional": true,
236 | "os": [
237 | "linux"
238 | ],
239 | "engines": {
240 | "node": ">=12"
241 | }
242 | },
243 | "node_modules/@esbuild/linux-riscv64": {
244 | "version": "0.19.12",
245 | "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.19.12.tgz",
246 | "integrity": "sha512-2MueBrlPQCw5dVJJpQdUYgeqIzDQgw3QtiAHUC4RBz9FXPrskyyU3VI1hw7C0BSKB9OduwSJ79FTCqtGMWqJHg==",
247 | "cpu": [
248 | "riscv64"
249 | ],
250 | "dev": true,
251 | "optional": true,
252 | "os": [
253 | "linux"
254 | ],
255 | "engines": {
256 | "node": ">=12"
257 | }
258 | },
259 | "node_modules/@esbuild/linux-s390x": {
260 | "version": "0.19.12",
261 | "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.19.12.tgz",
262 | "integrity": "sha512-+Pil1Nv3Umes4m3AZKqA2anfhJiVmNCYkPchwFJNEJN5QxmTs1uzyy4TvmDrCRNT2ApwSari7ZIgrPeUx4UZDg==",
263 | "cpu": [
264 | "s390x"
265 | ],
266 | "dev": true,
267 | "optional": true,
268 | "os": [
269 | "linux"
270 | ],
271 | "engines": {
272 | "node": ">=12"
273 | }
274 | },
275 | "node_modules/@esbuild/linux-x64": {
276 | "version": "0.19.12",
277 | "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.19.12.tgz",
278 | "integrity": "sha512-B71g1QpxfwBvNrfyJdVDexenDIt1CiDN1TIXLbhOw0KhJzE78KIFGX6OJ9MrtC0oOqMWf+0xop4qEU8JrJTwCg==",
279 | "cpu": [
280 | "x64"
281 | ],
282 | "dev": true,
283 | "optional": true,
284 | "os": [
285 | "linux"
286 | ],
287 | "engines": {
288 | "node": ">=12"
289 | }
290 | },
291 | "node_modules/@esbuild/netbsd-x64": {
292 | "version": "0.19.12",
293 | "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.19.12.tgz",
294 | "integrity": "sha512-3ltjQ7n1owJgFbuC61Oj++XhtzmymoCihNFgT84UAmJnxJfm4sYCiSLTXZtE00VWYpPMYc+ZQmB6xbSdVh0JWA==",
295 | "cpu": [
296 | "x64"
297 | ],
298 | "dev": true,
299 | "optional": true,
300 | "os": [
301 | "netbsd"
302 | ],
303 | "engines": {
304 | "node": ">=12"
305 | }
306 | },
307 | "node_modules/@esbuild/openbsd-x64": {
308 | "version": "0.19.12",
309 | "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.19.12.tgz",
310 | "integrity": "sha512-RbrfTB9SWsr0kWmb9srfF+L933uMDdu9BIzdA7os2t0TXhCRjrQyCeOt6wVxr79CKD4c+p+YhCj31HBkYcXebw==",
311 | "cpu": [
312 | "x64"
313 | ],
314 | "dev": true,
315 | "optional": true,
316 | "os": [
317 | "openbsd"
318 | ],
319 | "engines": {
320 | "node": ">=12"
321 | }
322 | },
323 | "node_modules/@esbuild/sunos-x64": {
324 | "version": "0.19.12",
325 | "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.19.12.tgz",
326 | "integrity": "sha512-HKjJwRrW8uWtCQnQOz9qcU3mUZhTUQvi56Q8DPTLLB+DawoiQdjsYq+j+D3s9I8VFtDr+F9CjgXKKC4ss89IeA==",
327 | "cpu": [
328 | "x64"
329 | ],
330 | "dev": true,
331 | "optional": true,
332 | "os": [
333 | "sunos"
334 | ],
335 | "engines": {
336 | "node": ">=12"
337 | }
338 | },
339 | "node_modules/@esbuild/win32-arm64": {
340 | "version": "0.19.12",
341 | "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.19.12.tgz",
342 | "integrity": "sha512-URgtR1dJnmGvX864pn1B2YUYNzjmXkuJOIqG2HdU62MVS4EHpU2946OZoTMnRUHklGtJdJZ33QfzdjGACXhn1A==",
343 | "cpu": [
344 | "arm64"
345 | ],
346 | "dev": true,
347 | "optional": true,
348 | "os": [
349 | "win32"
350 | ],
351 | "engines": {
352 | "node": ">=12"
353 | }
354 | },
355 | "node_modules/@esbuild/win32-ia32": {
356 | "version": "0.19.12",
357 | "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.19.12.tgz",
358 | "integrity": "sha512-+ZOE6pUkMOJfmxmBZElNOx72NKpIa/HFOMGzu8fqzQJ5kgf6aTGrcJaFsNiVMH4JKpMipyK+7k0n2UXN7a8YKQ==",
359 | "cpu": [
360 | "ia32"
361 | ],
362 | "dev": true,
363 | "optional": true,
364 | "os": [
365 | "win32"
366 | ],
367 | "engines": {
368 | "node": ">=12"
369 | }
370 | },
371 | "node_modules/@esbuild/win32-x64": {
372 | "version": "0.19.12",
373 | "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.19.12.tgz",
374 | "integrity": "sha512-T1QyPSDCyMXaO3pzBkF96E8xMkiRYbUEZADd29SyPGabqxMViNoii+NcK7eWJAEoU6RZyEm5lVSIjTmcdoB9HA==",
375 | "cpu": [
376 | "x64"
377 | ],
378 | "dev": true,
379 | "optional": true,
380 | "os": [
381 | "win32"
382 | ],
383 | "engines": {
384 | "node": ">=12"
385 | }
386 | },
387 | "node_modules/@mediapipe/tasks-vision": {
388 | "version": "0.10.12",
389 | "resolved": "https://registry.npmjs.org/@mediapipe/tasks-vision/-/tasks-vision-0.10.12.tgz",
390 | "integrity": "sha512-688Vukid7hvGmx+7hzS/EQ3Q4diz4eeX4/FYDw8f/t56UjFueD8LTvA2rX5BCIwvT0oy8QHKh5uKIyct1AOFtQ=="
391 | },
392 | "node_modules/@rollup/rollup-android-arm-eabi": {
393 | "version": "4.13.0",
394 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.13.0.tgz",
395 | "integrity": "sha512-5ZYPOuaAqEH/W3gYsRkxQATBW3Ii1MfaT4EQstTnLKViLi2gLSQmlmtTpGucNP3sXEpOiI5tdGhjdE111ekyEg==",
396 | "cpu": [
397 | "arm"
398 | ],
399 | "dev": true,
400 | "optional": true,
401 | "os": [
402 | "android"
403 | ]
404 | },
405 | "node_modules/@rollup/rollup-android-arm64": {
406 | "version": "4.13.0",
407 | "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.13.0.tgz",
408 | "integrity": "sha512-BSbaCmn8ZadK3UAQdlauSvtaJjhlDEjS5hEVVIN3A4bbl3X+otyf/kOJV08bYiRxfejP3DXFzO2jz3G20107+Q==",
409 | "cpu": [
410 | "arm64"
411 | ],
412 | "dev": true,
413 | "optional": true,
414 | "os": [
415 | "android"
416 | ]
417 | },
418 | "node_modules/@rollup/rollup-darwin-arm64": {
419 | "version": "4.13.0",
420 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.13.0.tgz",
421 | "integrity": "sha512-Ovf2evVaP6sW5Ut0GHyUSOqA6tVKfrTHddtmxGQc1CTQa1Cw3/KMCDEEICZBbyppcwnhMwcDce9ZRxdWRpVd6g==",
422 | "cpu": [
423 | "arm64"
424 | ],
425 | "dev": true,
426 | "optional": true,
427 | "os": [
428 | "darwin"
429 | ]
430 | },
431 | "node_modules/@rollup/rollup-darwin-x64": {
432 | "version": "4.13.0",
433 | "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.13.0.tgz",
434 | "integrity": "sha512-U+Jcxm89UTK592vZ2J9st9ajRv/hrwHdnvyuJpa5A2ngGSVHypigidkQJP+YiGL6JODiUeMzkqQzbCG3At81Gg==",
435 | "cpu": [
436 | "x64"
437 | ],
438 | "dev": true,
439 | "optional": true,
440 | "os": [
441 | "darwin"
442 | ]
443 | },
444 | "node_modules/@rollup/rollup-linux-arm-gnueabihf": {
445 | "version": "4.13.0",
446 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.13.0.tgz",
447 | "integrity": "sha512-8wZidaUJUTIR5T4vRS22VkSMOVooG0F4N+JSwQXWSRiC6yfEsFMLTYRFHvby5mFFuExHa/yAp9juSphQQJAijQ==",
448 | "cpu": [
449 | "arm"
450 | ],
451 | "dev": true,
452 | "optional": true,
453 | "os": [
454 | "linux"
455 | ]
456 | },
457 | "node_modules/@rollup/rollup-linux-arm64-gnu": {
458 | "version": "4.13.0",
459 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.13.0.tgz",
460 | "integrity": "sha512-Iu0Kno1vrD7zHQDxOmvweqLkAzjxEVqNhUIXBsZ8hu8Oak7/5VTPrxOEZXYC1nmrBVJp0ZcL2E7lSuuOVaE3+w==",
461 | "cpu": [
462 | "arm64"
463 | ],
464 | "dev": true,
465 | "optional": true,
466 | "os": [
467 | "linux"
468 | ]
469 | },
470 | "node_modules/@rollup/rollup-linux-arm64-musl": {
471 | "version": "4.13.0",
472 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.13.0.tgz",
473 | "integrity": "sha512-C31QrW47llgVyrRjIwiOwsHFcaIwmkKi3PCroQY5aVq4H0A5v/vVVAtFsI1nfBngtoRpeREvZOkIhmRwUKkAdw==",
474 | "cpu": [
475 | "arm64"
476 | ],
477 | "dev": true,
478 | "optional": true,
479 | "os": [
480 | "linux"
481 | ]
482 | },
483 | "node_modules/@rollup/rollup-linux-riscv64-gnu": {
484 | "version": "4.13.0",
485 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.13.0.tgz",
486 | "integrity": "sha512-Oq90dtMHvthFOPMl7pt7KmxzX7E71AfyIhh+cPhLY9oko97Zf2C9tt/XJD4RgxhaGeAraAXDtqxvKE1y/j35lA==",
487 | "cpu": [
488 | "riscv64"
489 | ],
490 | "dev": true,
491 | "optional": true,
492 | "os": [
493 | "linux"
494 | ]
495 | },
496 | "node_modules/@rollup/rollup-linux-x64-gnu": {
497 | "version": "4.13.0",
498 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.13.0.tgz",
499 | "integrity": "sha512-yUD/8wMffnTKuiIsl6xU+4IA8UNhQ/f1sAnQebmE/lyQ8abjsVyDkyRkWop0kdMhKMprpNIhPmYlCxgHrPoXoA==",
500 | "cpu": [
501 | "x64"
502 | ],
503 | "dev": true,
504 | "optional": true,
505 | "os": [
506 | "linux"
507 | ]
508 | },
509 | "node_modules/@rollup/rollup-linux-x64-musl": {
510 | "version": "4.13.0",
511 | "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.13.0.tgz",
512 | "integrity": "sha512-9RyNqoFNdF0vu/qqX63fKotBh43fJQeYC98hCaf89DYQpv+xu0D8QFSOS0biA7cGuqJFOc1bJ+m2rhhsKcw1hw==",
513 | "cpu": [
514 | "x64"
515 | ],
516 | "dev": true,
517 | "optional": true,
518 | "os": [
519 | "linux"
520 | ]
521 | },
522 | "node_modules/@rollup/rollup-win32-arm64-msvc": {
523 | "version": "4.13.0",
524 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.13.0.tgz",
525 | "integrity": "sha512-46ue8ymtm/5PUU6pCvjlic0z82qWkxv54GTJZgHrQUuZnVH+tvvSP0LsozIDsCBFO4VjJ13N68wqrKSeScUKdA==",
526 | "cpu": [
527 | "arm64"
528 | ],
529 | "dev": true,
530 | "optional": true,
531 | "os": [
532 | "win32"
533 | ]
534 | },
535 | "node_modules/@rollup/rollup-win32-ia32-msvc": {
536 | "version": "4.13.0",
537 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.13.0.tgz",
538 | "integrity": "sha512-P5/MqLdLSlqxbeuJ3YDeX37srC8mCflSyTrUsgbU1c/U9j6l2g2GiIdYaGD9QjdMQPMSgYm7hgg0551wHyIluw==",
539 | "cpu": [
540 | "ia32"
541 | ],
542 | "dev": true,
543 | "optional": true,
544 | "os": [
545 | "win32"
546 | ]
547 | },
548 | "node_modules/@rollup/rollup-win32-x64-msvc": {
549 | "version": "4.13.0",
550 | "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.13.0.tgz",
551 | "integrity": "sha512-UKXUQNbO3DOhzLRwHSpa0HnhhCgNODvfoPWv2FCXme8N/ANFfhIPMGuOT+QuKd16+B5yxZ0HdpNlqPvTMS1qfw==",
552 | "cpu": [
553 | "x64"
554 | ],
555 | "dev": true,
556 | "optional": true,
557 | "os": [
558 | "win32"
559 | ]
560 | },
561 | "node_modules/@tensorflow/tfjs": {
562 | "version": "4.17.0",
563 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs/-/tfjs-4.17.0.tgz",
564 | "integrity": "sha512-yXRBhpM3frlNA/YaPp6HNk9EfIi8han5RYeQA3R8OCa0Od+AfoG1PUmlxV8fE2wCorlGVyHsgpiJ6M9YZPB56w==",
565 | "dependencies": {
566 | "@tensorflow/tfjs-backend-cpu": "4.17.0",
567 | "@tensorflow/tfjs-backend-webgl": "4.17.0",
568 | "@tensorflow/tfjs-converter": "4.17.0",
569 | "@tensorflow/tfjs-core": "4.17.0",
570 | "@tensorflow/tfjs-data": "4.17.0",
571 | "@tensorflow/tfjs-layers": "4.17.0",
572 | "argparse": "^1.0.10",
573 | "chalk": "^4.1.0",
574 | "core-js": "3.29.1",
575 | "regenerator-runtime": "^0.13.5",
576 | "yargs": "^16.0.3"
577 | },
578 | "bin": {
579 | "tfjs-custom-module": "dist/tools/custom_module/cli.js"
580 | }
581 | },
582 | "node_modules/@tensorflow/tfjs-backend-cpu": {
583 | "version": "4.17.0",
584 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-cpu/-/tfjs-backend-cpu-4.17.0.tgz",
585 | "integrity": "sha512-2VSCHnX9qhYTjw9HiVwTBSnRVlntKXeBlK7aSVsmZfHGwWE2faErTtO7bWmqNqw0U7gyznJbVAjlow/p+0RNGw==",
586 | "dependencies": {
587 | "@types/seedrandom": "^2.4.28",
588 | "seedrandom": "^3.0.5"
589 | },
590 | "engines": {
591 | "yarn": ">= 1.3.2"
592 | },
593 | "peerDependencies": {
594 | "@tensorflow/tfjs-core": "4.17.0"
595 | }
596 | },
597 | "node_modules/@tensorflow/tfjs-backend-webgl": {
598 | "version": "4.17.0",
599 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-backend-webgl/-/tfjs-backend-webgl-4.17.0.tgz",
600 | "integrity": "sha512-CC5GsGECCd7eYAUaKq0XJ48FjEZdgXZWPxgUYx4djvfUx5fQPp35hCSP9w/k463jllBMbjl2tKRg8u7Ia/LYzg==",
601 | "dependencies": {
602 | "@tensorflow/tfjs-backend-cpu": "4.17.0",
603 | "@types/offscreencanvas": "~2019.3.0",
604 | "@types/seedrandom": "^2.4.28",
605 | "seedrandom": "^3.0.5"
606 | },
607 | "engines": {
608 | "yarn": ">= 1.3.2"
609 | },
610 | "peerDependencies": {
611 | "@tensorflow/tfjs-core": "4.17.0"
612 | }
613 | },
614 | "node_modules/@tensorflow/tfjs-converter": {
615 | "version": "4.17.0",
616 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-converter/-/tfjs-converter-4.17.0.tgz",
617 | "integrity": "sha512-qFxIjPfomCuTrYxsFjtKbi3QfdmTTCWo+RvqD64oCMS0sjp7sUDNhJyKDoLx6LZhXlwXpHIVDJctLMRMwet0Zw==",
618 | "peerDependencies": {
619 | "@tensorflow/tfjs-core": "4.17.0"
620 | }
621 | },
622 | "node_modules/@tensorflow/tfjs-core": {
623 | "version": "4.17.0",
624 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-core/-/tfjs-core-4.17.0.tgz",
625 | "integrity": "sha512-v9Q5430EnRpyhWNd9LVgXadciKvxLiq+sTrLKRowh26BHyAsams4tZIgX3lFKjB7b90p+FYifVMcqLTTHgjGpQ==",
626 | "dependencies": {
627 | "@types/long": "^4.0.1",
628 | "@types/offscreencanvas": "~2019.7.0",
629 | "@types/seedrandom": "^2.4.28",
630 | "@webgpu/types": "0.1.38",
631 | "long": "4.0.0",
632 | "node-fetch": "~2.6.1",
633 | "seedrandom": "^3.0.5"
634 | },
635 | "engines": {
636 | "yarn": ">= 1.3.2"
637 | }
638 | },
639 | "node_modules/@tensorflow/tfjs-core/node_modules/@types/offscreencanvas": {
640 | "version": "2019.7.3",
641 | "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.7.3.tgz",
642 | "integrity": "sha512-ieXiYmgSRXUDeOntE1InxjWyvEelZGP63M+cGuquuRLuIKKT1osnkXjxev9B7d1nXSug5vpunx+gNlbVxMlC9A=="
643 | },
644 | "node_modules/@tensorflow/tfjs-data": {
645 | "version": "4.17.0",
646 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-data/-/tfjs-data-4.17.0.tgz",
647 | "integrity": "sha512-aPKrDFip+gXicWOFALeNT7KKQjRXFkHd/hNe/zs4mCFcIN00hy1PkZ6xkYsgrsdLDQMBSGeS4B4ZM0k5Cs88QA==",
648 | "dependencies": {
649 | "@types/node-fetch": "^2.1.2",
650 | "node-fetch": "~2.6.1",
651 | "string_decoder": "^1.3.0"
652 | },
653 | "peerDependencies": {
654 | "@tensorflow/tfjs-core": "4.17.0",
655 | "seedrandom": "^3.0.5"
656 | }
657 | },
658 | "node_modules/@tensorflow/tfjs-layers": {
659 | "version": "4.17.0",
660 | "resolved": "https://registry.npmjs.org/@tensorflow/tfjs-layers/-/tfjs-layers-4.17.0.tgz",
661 | "integrity": "sha512-DEE0zRKvf3LJ0EcvG5XouJYOgFGWYAneZ0K1d23969z7LfSyqVmBdLC6BTwdLKuJk3ouUJIKXU1TcpFmjDuh7g==",
662 | "peerDependencies": {
663 | "@tensorflow/tfjs-core": "4.17.0"
664 | }
665 | },
666 | "node_modules/@types/estree": {
667 | "version": "1.0.5",
668 | "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz",
669 | "integrity": "sha512-/kYRxGDLWzHOB7q+wtSUQlFrtcdUccpfy+X+9iMBpHK8QLLhx2wIPYuS5DYtR9Wa/YlZAbIovy7qVdB1Aq6Lyw==",
670 | "dev": true
671 | },
672 | "node_modules/@types/long": {
673 | "version": "4.0.2",
674 | "resolved": "https://registry.npmjs.org/@types/long/-/long-4.0.2.tgz",
675 | "integrity": "sha512-MqTGEo5bj5t157U6fA/BiDynNkn0YknVdh48CMPkTSpFTVmvao5UQmm7uEF6xBEo7qIMAlY/JSleYaE6VOdpaA=="
676 | },
677 | "node_modules/@types/node": {
678 | "version": "20.11.28",
679 | "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.28.tgz",
680 | "integrity": "sha512-M/GPWVS2wLkSkNHVeLkrF2fD5Lx5UC4PxA0uZcKc6QqbIQUJyW1jVjueJYi1z8n0I5PxYrtpnPnWglE+y9A0KA==",
681 | "dependencies": {
682 | "undici-types": "~5.26.4"
683 | }
684 | },
685 | "node_modules/@types/node-fetch": {
686 | "version": "2.6.11",
687 | "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz",
688 | "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==",
689 | "dependencies": {
690 | "@types/node": "*",
691 | "form-data": "^4.0.0"
692 | }
693 | },
694 | "node_modules/@types/offscreencanvas": {
695 | "version": "2019.3.0",
696 | "resolved": "https://registry.npmjs.org/@types/offscreencanvas/-/offscreencanvas-2019.3.0.tgz",
697 | "integrity": "sha512-esIJx9bQg+QYF0ra8GnvfianIY8qWB0GBx54PK5Eps6m+xTj86KLavHv6qDhzKcu5UUOgNfJ2pWaIIV7TRUd9Q=="
698 | },
699 | "node_modules/@types/seedrandom": {
700 | "version": "2.4.34",
701 | "resolved": "https://registry.npmjs.org/@types/seedrandom/-/seedrandom-2.4.34.tgz",
702 | "integrity": "sha512-ytDiArvrn/3Xk6/vtylys5tlY6eo7Ane0hvcx++TKo6RxQXuVfW0AF/oeWqAj9dN29SyhtawuXstgmPlwNcv/A=="
703 | },
704 | "node_modules/@webgpu/types": {
705 | "version": "0.1.38",
706 | "resolved": "https://registry.npmjs.org/@webgpu/types/-/types-0.1.38.tgz",
707 | "integrity": "sha512-7LrhVKz2PRh+DD7+S+PVaFd5HxaWQvoMqBbsV9fNJO1pjUs1P8bM2vQVNfk+3URTqbuTI7gkXi0rfsN0IadoBA=="
708 | },
709 | "node_modules/ansi-regex": {
710 | "version": "5.0.1",
711 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz",
712 | "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==",
713 | "engines": {
714 | "node": ">=8"
715 | }
716 | },
717 | "node_modules/ansi-styles": {
718 | "version": "4.3.0",
719 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
720 | "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
721 | "dependencies": {
722 | "color-convert": "^2.0.1"
723 | },
724 | "engines": {
725 | "node": ">=8"
726 | },
727 | "funding": {
728 | "url": "https://github.com/chalk/ansi-styles?sponsor=1"
729 | }
730 | },
731 | "node_modules/argparse": {
732 | "version": "1.0.10",
733 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz",
734 | "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==",
735 | "dependencies": {
736 | "sprintf-js": "~1.0.2"
737 | }
738 | },
739 | "node_modules/asynckit": {
740 | "version": "0.4.0",
741 | "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
742 | "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q=="
743 | },
744 | "node_modules/chalk": {
745 | "version": "4.1.2",
746 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
747 | "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
748 | "dependencies": {
749 | "ansi-styles": "^4.1.0",
750 | "supports-color": "^7.1.0"
751 | },
752 | "engines": {
753 | "node": ">=10"
754 | },
755 | "funding": {
756 | "url": "https://github.com/chalk/chalk?sponsor=1"
757 | }
758 | },
759 | "node_modules/cliui": {
760 | "version": "7.0.4",
761 | "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz",
762 | "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==",
763 | "dependencies": {
764 | "string-width": "^4.2.0",
765 | "strip-ansi": "^6.0.0",
766 | "wrap-ansi": "^7.0.0"
767 | }
768 | },
769 | "node_modules/color-convert": {
770 | "version": "2.0.1",
771 | "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
772 | "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
773 | "dependencies": {
774 | "color-name": "~1.1.4"
775 | },
776 | "engines": {
777 | "node": ">=7.0.0"
778 | }
779 | },
780 | "node_modules/color-name": {
781 | "version": "1.1.4",
782 | "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
783 | "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
784 | },
785 | "node_modules/combined-stream": {
786 | "version": "1.0.8",
787 | "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
788 | "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
789 | "dependencies": {
790 | "delayed-stream": "~1.0.0"
791 | },
792 | "engines": {
793 | "node": ">= 0.8"
794 | }
795 | },
796 | "node_modules/core-js": {
797 | "version": "3.29.1",
798 | "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.29.1.tgz",
799 | "integrity": "sha512-+jwgnhg6cQxKYIIjGtAHq2nwUOolo9eoFZ4sHfUH09BLXBgxnH4gA0zEd+t+BO2cNB8idaBtZFcFTRjQJRJmAw==",
800 | "hasInstallScript": true,
801 | "funding": {
802 | "type": "opencollective",
803 | "url": "https://opencollective.com/core-js"
804 | }
805 | },
806 | "node_modules/delayed-stream": {
807 | "version": "1.0.0",
808 | "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
809 | "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
810 | "engines": {
811 | "node": ">=0.4.0"
812 | }
813 | },
814 | "node_modules/emoji-regex": {
815 | "version": "8.0.0",
816 | "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz",
817 | "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="
818 | },
819 | "node_modules/esbuild": {
820 | "version": "0.19.12",
821 | "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.12.tgz",
822 | "integrity": "sha512-aARqgq8roFBj054KvQr5f1sFu0D65G+miZRCuJyJ0G13Zwx7vRar5Zhn2tkQNzIXcBrNVsv/8stehpj+GAjgbg==",
823 | "dev": true,
824 | "hasInstallScript": true,
825 | "bin": {
826 | "esbuild": "bin/esbuild"
827 | },
828 | "engines": {
829 | "node": ">=12"
830 | },
831 | "optionalDependencies": {
832 | "@esbuild/aix-ppc64": "0.19.12",
833 | "@esbuild/android-arm": "0.19.12",
834 | "@esbuild/android-arm64": "0.19.12",
835 | "@esbuild/android-x64": "0.19.12",
836 | "@esbuild/darwin-arm64": "0.19.12",
837 | "@esbuild/darwin-x64": "0.19.12",
838 | "@esbuild/freebsd-arm64": "0.19.12",
839 | "@esbuild/freebsd-x64": "0.19.12",
840 | "@esbuild/linux-arm": "0.19.12",
841 | "@esbuild/linux-arm64": "0.19.12",
842 | "@esbuild/linux-ia32": "0.19.12",
843 | "@esbuild/linux-loong64": "0.19.12",
844 | "@esbuild/linux-mips64el": "0.19.12",
845 | "@esbuild/linux-ppc64": "0.19.12",
846 | "@esbuild/linux-riscv64": "0.19.12",
847 | "@esbuild/linux-s390x": "0.19.12",
848 | "@esbuild/linux-x64": "0.19.12",
849 | "@esbuild/netbsd-x64": "0.19.12",
850 | "@esbuild/openbsd-x64": "0.19.12",
851 | "@esbuild/sunos-x64": "0.19.12",
852 | "@esbuild/win32-arm64": "0.19.12",
853 | "@esbuild/win32-ia32": "0.19.12",
854 | "@esbuild/win32-x64": "0.19.12"
855 | }
856 | },
857 | "node_modules/escalade": {
858 | "version": "3.1.2",
859 | "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz",
860 | "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==",
861 | "engines": {
862 | "node": ">=6"
863 | }
864 | },
865 | "node_modules/form-data": {
866 | "version": "4.0.0",
867 | "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
868 | "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
869 | "dependencies": {
870 | "asynckit": "^0.4.0",
871 | "combined-stream": "^1.0.8",
872 | "mime-types": "^2.1.12"
873 | },
874 | "engines": {
875 | "node": ">= 6"
876 | }
877 | },
878 | "node_modules/fsevents": {
879 | "version": "2.3.3",
880 | "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
881 | "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
882 | "dev": true,
883 | "hasInstallScript": true,
884 | "optional": true,
885 | "os": [
886 | "darwin"
887 | ],
888 | "engines": {
889 | "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
890 | }
891 | },
892 | "node_modules/get-caller-file": {
893 | "version": "2.0.5",
894 | "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz",
895 | "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==",
896 | "engines": {
897 | "node": "6.* || 8.* || >= 10.*"
898 | }
899 | },
900 | "node_modules/has-flag": {
901 | "version": "4.0.0",
902 | "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
903 | "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
904 | "engines": {
905 | "node": ">=8"
906 | }
907 | },
908 | "node_modules/is-fullwidth-code-point": {
909 | "version": "3.0.0",
910 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz",
911 | "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==",
912 | "engines": {
913 | "node": ">=8"
914 | }
915 | },
916 | "node_modules/long": {
917 | "version": "4.0.0",
918 | "resolved": "https://registry.npmjs.org/long/-/long-4.0.0.tgz",
919 | "integrity": "sha512-XsP+KhQif4bjX1kbuSiySJFNAehNxgLb6hPRGJ9QsUr8ajHkuXGdrHmFUTUUXhDwVX2R5bY4JNZEwbUiMhV+MA=="
920 | },
921 | "node_modules/mime-db": {
922 | "version": "1.52.0",
923 | "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz",
924 | "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==",
925 | "engines": {
926 | "node": ">= 0.6"
927 | }
928 | },
929 | "node_modules/mime-types": {
930 | "version": "2.1.35",
931 | "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz",
932 | "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==",
933 | "dependencies": {
934 | "mime-db": "1.52.0"
935 | },
936 | "engines": {
937 | "node": ">= 0.6"
938 | }
939 | },
940 | "node_modules/nanoid": {
941 | "version": "3.3.7",
942 | "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz",
943 | "integrity": "sha512-eSRppjcPIatRIMC1U6UngP8XFcz8MQWGQdt1MTBQ7NaAmvXDfvNxbvWV3x2y6CdEUciCSsDHDQZbhYaB8QEo2g==",
944 | "dev": true,
945 | "funding": [
946 | {
947 | "type": "github",
948 | "url": "https://github.com/sponsors/ai"
949 | }
950 | ],
951 | "bin": {
952 | "nanoid": "bin/nanoid.cjs"
953 | },
954 | "engines": {
955 | "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
956 | }
957 | },
958 | "node_modules/node-fetch": {
959 | "version": "2.6.13",
960 | "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.13.tgz",
961 | "integrity": "sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA==",
962 | "dependencies": {
963 | "whatwg-url": "^5.0.0"
964 | },
965 | "engines": {
966 | "node": "4.x || >=6.0.0"
967 | },
968 | "peerDependencies": {
969 | "encoding": "^0.1.0"
970 | },
971 | "peerDependenciesMeta": {
972 | "encoding": {
973 | "optional": true
974 | }
975 | }
976 | },
977 | "node_modules/picocolors": {
978 | "version": "1.0.0",
979 | "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz",
980 | "integrity": "sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==",
981 | "dev": true
982 | },
983 | "node_modules/postcss": {
984 | "version": "8.4.36",
985 | "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.36.tgz",
986 | "integrity": "sha512-/n7eumA6ZjFHAsbX30yhHup/IMkOmlmvtEi7P+6RMYf+bGJSUHc3geH4a0NSZxAz/RJfiS9tooCTs9LAVYUZKw==",
987 | "dev": true,
988 | "funding": [
989 | {
990 | "type": "opencollective",
991 | "url": "https://opencollective.com/postcss/"
992 | },
993 | {
994 | "type": "tidelift",
995 | "url": "https://tidelift.com/funding/github/npm/postcss"
996 | },
997 | {
998 | "type": "github",
999 | "url": "https://github.com/sponsors/ai"
1000 | }
1001 | ],
1002 | "dependencies": {
1003 | "nanoid": "^3.3.7",
1004 | "picocolors": "^1.0.0",
1005 | "source-map-js": "^1.1.0"
1006 | },
1007 | "engines": {
1008 | "node": "^10 || ^12 || >=14"
1009 | }
1010 | },
1011 | "node_modules/regenerator-runtime": {
1012 | "version": "0.13.11",
1013 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz",
1014 | "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg=="
1015 | },
1016 | "node_modules/require-directory": {
1017 | "version": "2.1.1",
1018 | "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
1019 | "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==",
1020 | "engines": {
1021 | "node": ">=0.10.0"
1022 | }
1023 | },
1024 | "node_modules/rollup": {
1025 | "version": "4.13.0",
1026 | "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.13.0.tgz",
1027 | "integrity": "sha512-3YegKemjoQnYKmsBlOHfMLVPPA5xLkQ8MHLLSw/fBrFaVkEayL51DilPpNNLq1exr98F2B1TzrV0FUlN3gWRPg==",
1028 | "dev": true,
1029 | "dependencies": {
1030 | "@types/estree": "1.0.5"
1031 | },
1032 | "bin": {
1033 | "rollup": "dist/bin/rollup"
1034 | },
1035 | "engines": {
1036 | "node": ">=18.0.0",
1037 | "npm": ">=8.0.0"
1038 | },
1039 | "optionalDependencies": {
1040 | "@rollup/rollup-android-arm-eabi": "4.13.0",
1041 | "@rollup/rollup-android-arm64": "4.13.0",
1042 | "@rollup/rollup-darwin-arm64": "4.13.0",
1043 | "@rollup/rollup-darwin-x64": "4.13.0",
1044 | "@rollup/rollup-linux-arm-gnueabihf": "4.13.0",
1045 | "@rollup/rollup-linux-arm64-gnu": "4.13.0",
1046 | "@rollup/rollup-linux-arm64-musl": "4.13.0",
1047 | "@rollup/rollup-linux-riscv64-gnu": "4.13.0",
1048 | "@rollup/rollup-linux-x64-gnu": "4.13.0",
1049 | "@rollup/rollup-linux-x64-musl": "4.13.0",
1050 | "@rollup/rollup-win32-arm64-msvc": "4.13.0",
1051 | "@rollup/rollup-win32-ia32-msvc": "4.13.0",
1052 | "@rollup/rollup-win32-x64-msvc": "4.13.0",
1053 | "fsevents": "~2.3.2"
1054 | }
1055 | },
1056 | "node_modules/safe-buffer": {
1057 | "version": "5.2.1",
1058 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
1059 | "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
1060 | "funding": [
1061 | {
1062 | "type": "github",
1063 | "url": "https://github.com/sponsors/feross"
1064 | },
1065 | {
1066 | "type": "patreon",
1067 | "url": "https://www.patreon.com/feross"
1068 | },
1069 | {
1070 | "type": "consulting",
1071 | "url": "https://feross.org/support"
1072 | }
1073 | ]
1074 | },
1075 | "node_modules/seedrandom": {
1076 | "version": "3.0.5",
1077 | "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
1078 | "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
1079 | },
1080 | "node_modules/source-map-js": {
1081 | "version": "1.1.0",
1082 | "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.1.0.tgz",
1083 | "integrity": "sha512-9vC2SfsJzlej6MAaMPLu8HiBSHGdRAJ9hVFYN1ibZoNkeanmDmLUcIrj6G9DGL7XMJ54AKg/G75akXl1/izTOw==",
1084 | "dev": true,
1085 | "engines": {
1086 | "node": ">=0.10.0"
1087 | }
1088 | },
1089 | "node_modules/sprintf-js": {
1090 | "version": "1.0.3",
1091 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz",
1092 | "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g=="
1093 | },
1094 | "node_modules/string_decoder": {
1095 | "version": "1.3.0",
1096 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
1097 | "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
1098 | "dependencies": {
1099 | "safe-buffer": "~5.2.0"
1100 | }
1101 | },
1102 | "node_modules/string-width": {
1103 | "version": "4.2.3",
1104 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
1105 | "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==",
1106 | "dependencies": {
1107 | "emoji-regex": "^8.0.0",
1108 | "is-fullwidth-code-point": "^3.0.0",
1109 | "strip-ansi": "^6.0.1"
1110 | },
1111 | "engines": {
1112 | "node": ">=8"
1113 | }
1114 | },
1115 | "node_modules/strip-ansi": {
1116 | "version": "6.0.1",
1117 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz",
1118 | "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==",
1119 | "dependencies": {
1120 | "ansi-regex": "^5.0.1"
1121 | },
1122 | "engines": {
1123 | "node": ">=8"
1124 | }
1125 | },
1126 | "node_modules/supports-color": {
1127 | "version": "7.2.0",
1128 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
1129 | "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
1130 | "dependencies": {
1131 | "has-flag": "^4.0.0"
1132 | },
1133 | "engines": {
1134 | "node": ">=8"
1135 | }
1136 | },
1137 | "node_modules/tr46": {
1138 | "version": "0.0.3",
1139 | "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
1140 | "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
1141 | },
1142 | "node_modules/undici-types": {
1143 | "version": "5.26.5",
1144 | "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz",
1145 | "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA=="
1146 | },
1147 | "node_modules/vite": {
1148 | "version": "5.1.6",
1149 | "resolved": "https://registry.npmjs.org/vite/-/vite-5.1.6.tgz",
1150 | "integrity": "sha512-yYIAZs9nVfRJ/AiOLCA91zzhjsHUgMjB+EigzFb6W2XTLO8JixBCKCjvhKZaye+NKYHCrkv3Oh50dH9EdLU2RA==",
1151 | "dev": true,
1152 | "dependencies": {
1153 | "esbuild": "^0.19.3",
1154 | "postcss": "^8.4.35",
1155 | "rollup": "^4.2.0"
1156 | },
1157 | "bin": {
1158 | "vite": "bin/vite.js"
1159 | },
1160 | "engines": {
1161 | "node": "^18.0.0 || >=20.0.0"
1162 | },
1163 | "funding": {
1164 | "url": "https://github.com/vitejs/vite?sponsor=1"
1165 | },
1166 | "optionalDependencies": {
1167 | "fsevents": "~2.3.3"
1168 | },
1169 | "peerDependencies": {
1170 | "@types/node": "^18.0.0 || >=20.0.0",
1171 | "less": "*",
1172 | "lightningcss": "^1.21.0",
1173 | "sass": "*",
1174 | "stylus": "*",
1175 | "sugarss": "*",
1176 | "terser": "^5.4.0"
1177 | },
1178 | "peerDependenciesMeta": {
1179 | "@types/node": {
1180 | "optional": true
1181 | },
1182 | "less": {
1183 | "optional": true
1184 | },
1185 | "lightningcss": {
1186 | "optional": true
1187 | },
1188 | "sass": {
1189 | "optional": true
1190 | },
1191 | "stylus": {
1192 | "optional": true
1193 | },
1194 | "sugarss": {
1195 | "optional": true
1196 | },
1197 | "terser": {
1198 | "optional": true
1199 | }
1200 | }
1201 | },
1202 | "node_modules/webidl-conversions": {
1203 | "version": "3.0.1",
1204 | "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
1205 | "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
1206 | },
1207 | "node_modules/whatwg-url": {
1208 | "version": "5.0.0",
1209 | "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
1210 | "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
1211 | "dependencies": {
1212 | "tr46": "~0.0.3",
1213 | "webidl-conversions": "^3.0.0"
1214 | }
1215 | },
1216 | "node_modules/wrap-ansi": {
1217 | "version": "7.0.0",
1218 | "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
1219 | "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==",
1220 | "dependencies": {
1221 | "ansi-styles": "^4.0.0",
1222 | "string-width": "^4.1.0",
1223 | "strip-ansi": "^6.0.0"
1224 | },
1225 | "engines": {
1226 | "node": ">=10"
1227 | },
1228 | "funding": {
1229 | "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
1230 | }
1231 | },
1232 | "node_modules/y18n": {
1233 | "version": "5.0.8",
1234 | "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
1235 | "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==",
1236 | "engines": {
1237 | "node": ">=10"
1238 | }
1239 | },
1240 | "node_modules/yargs": {
1241 | "version": "16.2.0",
1242 | "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz",
1243 | "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==",
1244 | "dependencies": {
1245 | "cliui": "^7.0.2",
1246 | "escalade": "^3.1.1",
1247 | "get-caller-file": "^2.0.5",
1248 | "require-directory": "^2.1.1",
1249 | "string-width": "^4.2.0",
1250 | "y18n": "^5.0.5",
1251 | "yargs-parser": "^20.2.2"
1252 | },
1253 | "engines": {
1254 | "node": ">=10"
1255 | }
1256 | },
1257 | "node_modules/yargs-parser": {
1258 | "version": "20.2.9",
1259 | "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz",
1260 | "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==",
1261 | "engines": {
1262 | "node": ">=10"
1263 | }
1264 | }
1265 | }
1266 | }
1267 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "fern-ar",
3 | "version": "1.0.0",
4 | "description": "augmented reality in web. A-Frame. Gesture Recognition",
5 | "main": "dist/fernar-gesture.prod.js",
6 | "type": "module",
7 | "scripts": {
8 | "build": "vite build --config vite.config.js"
9 | },
10 | "repository": {
11 | "type": "git",
12 | "url": "git+https://github.com/rylanpeng/fern-ar-js.git"
13 | },
14 | "keywords": [
15 | "aframe"
16 | ],
17 | "author": "rylanpeng",
18 | "license": "MIT",
19 | "bugs": {
20 | "url": "https://github.com/rylanpeng/fern-ar-js/issues"
21 | },
22 | "homepage": "https://github.com/rylanpeng/fern-ar-js#readme",
23 | "devDependencies": {
24 | "vite": "^5.1.5"
25 | },
26 | "dependencies": {
27 | "@mediapipe/tasks-vision": "^0.10.12",
28 | "@tensorflow/tfjs": "^4.17.0"
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/src/aframe.js:
--------------------------------------------------------------------------------
1 | import { Gesture } from "./gesture.js";
2 | import { DrawingUtils, HandLandmarker } from "@mediapipe/tasks-vision";
3 | let gestureRecognizer = null;
4 | let finishMLInit = false;
5 | let debugMode = false;
6 | AFRAME.registerSystem("fernar-gesture-system", {
7 | schema: {},
8 | init: function () {
9 | this.gestureEntityMap = new Map();
10 | },
11 | start: function () {},
12 | tick: function () {},
13 | pause: function () {},
14 | play: function () {},
15 | addGestures: function (gestures, entity) {
16 | gestures.forEach((gesture) => {
17 | if (debugMode) {
18 | console.log(`Adding gesture ${+gesture} to entity`);
19 | }
20 | if (this.gestureEntityMap.has(+gesture)) {
21 | this.gestureEntityMap.get(+gesture).add(entity);
22 | } else {
23 | this.gestureEntityMap.set(+gesture, new Set([entity]));
24 | }
25 | });
26 | },
27 |
28 | notify: function (hand, gesture, landmarks) {
29 | if (landmarks.length > 20) {
30 | const sums = landmarks.reduce(
31 | (acc, { x, y, z }) => ({
32 | sumX: acc.sumX + x,
33 | sumY: acc.sumY + y,
34 | sumZ: acc.sumZ + z,
35 | }),
36 | { sumX: 0, sumY: 0, sumZ: 0 }
37 | );
38 |
39 | const totalElements = landmarks.length;
40 | const { sumX, sumY, sumZ } = sums;
41 | const averageX = sumX / totalElements;
42 | const averageY = sumY / totalElements;
43 | const averageZ = sumZ / totalElements;
44 |
45 | if (this.gestureEntityMap.has(+gesture)) {
46 | let entities = this.gestureEntityMap.get(+gesture);
47 | entities.forEach((entity, _) => {
48 | if (debugMode) {
49 | console.log(`emit fernar-gesture-event-${+gesture}`);
50 | }
51 | entity.emit(`fernar-gesture-event-${+gesture}`, {
52 | position: [averageX, averageY, averageZ],
53 | hand,
54 | });
55 | });
56 | }
57 | }
58 | },
59 | });
60 |
61 | AFRAME.registerComponent("update-plane-rotation", {
62 | tick: function () {
63 | const cameraRotation = this.el.sceneEl.camera.el.object3D.rotation.clone();
64 | this.el.object3D.rotation.copy(cameraRotation);
65 | },
66 | });
67 |
68 | AFRAME.registerComponent("fernar-gesture", {
69 | dependencies: ["fernar-gesture-system"],
70 | schema: {
71 | drawLandmarker: { type: "boolean", default: true },
72 | threshold: { type: "int", default: 10 },
73 | confidence: { type: "number", default: 0.7 },
74 | planePosition: { type: "vec3", default: { x: -6, y: 3, z: -7 } },
75 | planeWidth: { type: "number", default: 5 },
76 | planeHeight: { type: "number", default: 5 },
77 | },
78 | init: function () {
79 | this.gestureCount = {
80 | Left: { gestureId: 0, count: 0 },
81 | Right: { gestureId: 0, count: 0 },
82 | };
83 | this.planePosition = `${this.data.planePosition.x} ${this.data.planePosition.y} ${this.data.planePosition.z}`;
84 | this.el.sceneEl.addEventListener("renderstart", () => {
85 | gestureRecognizer = new Gesture();
86 | this.threshold = this.data.threshold;
87 | this.confidence = this.data.confidence;
88 |
89 | this.assetsElement = document.createElement("a-assets");
90 | this.el.sceneEl.appendChild(this.assetsElement);
91 |
92 | this.canvasElement = document.createElement("canvas");
93 | this.canvasElement.setAttribute("id", "fernar-canvas");
94 | this.assetsElement.appendChild(this.canvasElement);
95 |
96 | this.video = document.createElement("video");
97 | this.video.setAttribute("id", "fernar-video");
98 | this.video.setAttribute("autoplay", "");
99 | this.video.setAttribute("muted", "");
100 | this.video.setAttribute("playsinline", "");
101 | this.assetsElement.appendChild(this.video);
102 |
103 | this.APlaneElement = document.createElement("a-plane");
104 | this.APlaneElement.setAttribute("width", this.data.planeWidth);
105 | this.APlaneElement.setAttribute("height", this.data.planeHeight);
106 | this.APlaneElement.setAttribute("position", this.planePosition);
107 |
108 | this.AVideoElement = document.createElement("a-plane");
109 | this.AVideoElement.setAttribute("src", "#fernar-video");
110 | this.AVideoElement.setAttribute("width", this.data.planeWidth);
111 | this.AVideoElement.setAttribute("height", this.data.planeHeight);
112 | this.AVideoElement.setAttribute("position", this.planePosition);
113 |
114 | const camera = this.el.sceneEl.querySelector("a-camera");
115 | camera.appendChild(this.AVideoElement);
116 | camera.appendChild(this.APlaneElement);
117 |
118 | navigator.mediaDevices.getUserMedia({ video: true }).then((stream) => {
119 | this.video.srcObject = stream;
120 | this.video.addEventListener("loadedmetadata", async () => {
121 | this.video.width = this.video.videoWidth;
122 | this.video.height = this.video.videoHeight;
123 | await gestureRecognizer.init();
124 | finishMLInit = true;
125 | gestureRecognizer.predict(this.video);
126 | });
127 | gestureRecognizer.result = ({ hands, landmarks, gestures }) => {
128 | if (this.data.drawLandmarker) {
129 | this._drawLandmarker(landmarks);
130 | }
131 | const resultLength = Math.min(
132 | hands.length,
133 | landmarks.length,
134 | gestures.length
135 | );
136 | for (let i = 0; i < resultLength; i++) {
137 | if (gestures[i].confidence < this.confidence) {
138 | continue;
139 | }
140 | let hand = hands[i][0].categoryName === "Left" ? "Left" : "Right";
141 | if (
142 | this.gestureCount[hand].count == 0 ||
143 | gestures[i].prediction != this.gestureCount[hand].gestureId
144 | ) {
145 | this.gestureCount[hand] = {
146 | gestureId: gestures[i].prediction,
147 | count: 1,
148 | };
149 | } else {
150 | this.gestureCount[hand].count += 1;
151 | }
152 | if (this.gestureCount[hand].count == this.threshold) {
153 | this.el.sceneEl.systems["fernar-gesture-system"].notify(
154 | hand,
155 | gestures[i].prediction,
156 | landmarks[i]
157 | );
158 | this.gestureCount[hand].count = 0;
159 | }
160 | if (debugMode) {
161 | console.log(
162 | `${hand} Hand => Gesture ID: ${this.gestureCount[hand].gestureId}, Count: ${this.gestureCount[hand].count}, Confidence: ${gestures[i].confidence}`
163 | );
164 | }
165 | }
166 | };
167 | });
168 | });
169 | },
170 | _drawLandmarker(landmarks) {
171 | this.canvasElement.width = this.video.width;
172 | this.canvasElement.height = this.video.height;
173 | this.canvasCtx = this.canvasElement.getContext("2d");
174 | this.canvasCtx.clearRect(
175 | 0,
176 | 0,
177 | this.canvasElement.width,
178 | this.canvasElement.height
179 | );
180 | const drawingUtils = new DrawingUtils(this.canvasCtx);
181 | for (const landmark of landmarks) {
182 | drawingUtils.drawConnectors(landmark, HandLandmarker.HAND_CONNECTIONS, {
183 | color: "#C273E5",
184 | lineWidth: 7,
185 | });
186 | drawingUtils.drawLandmarks(landmark, {
187 | color: "#7E369E",
188 | lineWidth: 1,
189 | });
190 | }
191 | },
192 | tick: function () {
193 | this.texture = new THREE.CanvasTexture(this.canvasElement);
194 | const material = new THREE.MeshBasicMaterial({
195 | map: this.texture,
196 | transparent: true,
197 | opacity: 0.5,
198 | });
199 | this.APlaneElement.getObject3D("mesh").material = material;
200 | this.texture.needsUpdate = true;
201 | },
202 | pause: function () {},
203 | play: function () {},
204 | });
205 |
206 | AFRAME.registerComponent("fernar-gesture-target", {
207 | dependencies: ["fernar-gesture-system"],
208 | schema: {
209 | gesture: { type: "array" },
210 | },
211 | init: function () {
212 | this.el.sceneEl.systems["fernar-gesture-system"].addGestures(
213 | this.data.gesture,
214 | this.el
215 | );
216 | },
217 | start: function () {},
218 | tick: function () {},
219 | pause: function () {},
220 | play: function () {},
221 | });
222 |
223 | async function updateModel(modelJson, modelBin, binModelPath) {
224 | // TODO: Set a timeout here
225 | await new Promise((resolve) => {
226 | const checkFinish = () => {
227 | if (finishMLInit) {
228 | resolve();
229 | } else {
230 | setTimeout(checkFinish, 100);
231 | }
232 | };
233 | checkFinish();
234 | });
235 | await gestureRecognizer.updateModel(modelJson, modelBin, binModelPath);
236 | }
237 |
238 | function setDebugMode(value) {
239 | debugMode = value;
240 | }
241 |
242 | export { updateModel, setDebugMode };
243 |
--------------------------------------------------------------------------------
/src/gesture.js:
--------------------------------------------------------------------------------
1 | import { HandLandmarker, FilesetResolver } from "@mediapipe/tasks-vision";
2 | import * as tf from "@tensorflow/tfjs";
3 | class Gesture {
4 | constructor() {
5 | this.lastVideoTime = -1;
6 | this.results = undefined;
7 | this.stopPredict = false;
8 | }
9 | async init() {
10 | try {
11 | const vision = await FilesetResolver.forVisionTasks(
12 | "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm"
13 | );
14 | this.handLandmarker = await HandLandmarker.createFromOptions(vision, {
15 | baseOptions: {
16 | modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
17 | delegate: "GPU",
18 | },
19 | runningMode: "VIDEO",
20 | numHands: 2,
21 | });
22 |
23 | let modelFiles = [];
24 | let response = await fetch(
25 | "https://cdn.jsdelivr.net/npm/fern-ar@latest/model/model.json"
26 | );
27 | modelFiles.push(new File([await response.text()], "model.json"));
28 | response = await fetch(
29 | "https://cdn.jsdelivr.net/npm/fern-ar@latest/model/model.weights.bin"
30 | );
31 | modelFiles.push(
32 | new File(
33 | [new Uint8Array(await response.arrayBuffer())],
34 | "model.weights.bin"
35 | )
36 | );
37 | this.model = await tf.loadLayersModel(tf.io.browserFiles(modelFiles));
38 | console.log("Model loaded successfully:", this.model);
39 | } catch (error) {
40 | console.error("Error loading model:", error);
41 | }
42 | }
43 | predict(video) {
44 | if (!this.stopPredict) {
45 | this.videoWidth = video.width;
46 | this.videoHeight = video.height;
47 |
48 | let startTimeMs = performance.now();
49 | if (this.lastVideoTime !== video.currentTime) {
50 | this.lastVideoTime = video.currentTime;
51 | this.results = this.handLandmarker.detectForVideo(video, startTimeMs);
52 | }
53 | const resultHands = this.results.handedness;
54 | const resultLandmarks = this.results.landmarks;
55 | const resultGestures = [];
56 | const promises = [];
57 | for (let i = 0; i < this.results.landmarks.length; i++) {
58 | const preProcessedLandmarkList = this._preProcessedLandmark(
59 | this.results.landmarks[i]
60 | );
61 | if (preProcessedLandmarkList.length === 42) {
62 | promises.push(
63 | this._predict(preProcessedLandmarkList).then(({prediction, confidence}) => {
64 | resultGestures.push({prediction: prediction, confidence: confidence});
65 | })
66 | );
67 | }
68 | }
69 | Promise.all(promises)
70 | .then(() => {
71 | this.result({
72 | hands: resultHands,
73 | landmarks: resultLandmarks,
74 | gestures: resultGestures,
75 | });
76 | })
77 | .catch((error) => {
78 | console.error("Error occurred during prediction:", error);
79 | });
80 | }
81 | window.requestAnimationFrame(() => {
82 | this.predict(video);
83 | });
84 | }
85 | async updateModel(modelJson, modelBin, binModelPath) {
86 | this.stopPredict = true;
87 | try {
88 | let modelFiles = [];
89 | modelFiles.push(new File([modelJson], "model.json"));
90 | modelFiles.push(new File([new Uint8Array(modelBin)], binModelPath));
91 | this.model = await tf.loadLayersModel(tf.io.browserFiles(modelFiles));
92 | console.log("Model loaded successfully:", this.model);
93 | } catch (error) {
94 | console.error("Error loading model:", error);
95 | }
96 | this.stopPredict = false;
97 | }
98 | _preProcessedLandmark(landmarks) {
99 | // calc_landmark_list
100 | const landmark_list = [];
101 | for (const landmark of landmarks) {
102 | const landmark_x = Math.min(
103 | Math.floor(landmark.x * this.videoWidth),
104 | this.videoWidth - 1
105 | );
106 | const landmark_y = Math.min(
107 | Math.floor(landmark.y * this.videoHeight),
108 | this.videoHeight - 1
109 | );
110 | landmark_list.push([landmark_x, landmark_y]);
111 | }
112 |
113 | // pre_process_landmark
114 | const temp_landmark_list = JSON.parse(JSON.stringify(landmark_list));
115 | let base_x = 0;
116 | let base_y = 0;
117 | for (let index = 0; index < temp_landmark_list.length; index++) {
118 | const landmark_point = temp_landmark_list[index];
119 | if (index === 0) {
120 | base_x = landmark_point[0];
121 | base_y = landmark_point[1];
122 | }
123 | temp_landmark_list[index][0] -= base_x;
124 | temp_landmark_list[index][1] -= base_y;
125 | }
126 | const flatLandmarkList = temp_landmark_list.flat();
127 | const max_value = Math.max(...flatLandmarkList.map(Math.abs));
128 | const normalize_ = (n) => n / max_value;
129 | const normalized_landmark_list = flatLandmarkList.map(normalize_);
130 | return normalized_landmark_list;
131 | }
132 | async _predict(landmark_list) {
133 | const inputTensor = tf.tensor(
134 | [landmark_list],
135 | [1, landmark_list.length],
136 | "float32"
137 | );
138 | const result = this.model.predict(inputTensor);
139 | const resultData = await result.data();
140 | const maxProbability = Math.max(...resultData);
141 | const resultIndex = resultData.indexOf(maxProbability);
142 | return {prediction: resultIndex, confidence: maxProbability};
143 | }
144 | }
145 | export { Gesture };
146 |
--------------------------------------------------------------------------------
/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from "vite";
2 |
3 | export default defineConfig({
4 | mode: "production",
5 | build: {
6 | outDir: "dist",
7 | emptyOutDir: true,
8 | sourcemap: "inline",
9 | minify: false,
10 | lib: {
11 | entry: "src/aframe.js",
12 | name: "FERNAR",
13 | fileName: () => "[name].prod.js",
14 | formats: ["iife"],
15 | },
16 | rollupOptions: {
17 | input: {
18 | "fernar-gesture": "./src/aframe.js",
19 | },
20 | },
21 | },
22 | });
23 |
--------------------------------------------------------------------------------
/website/assets/floor.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/website/assets/floor.jpg
--------------------------------------------------------------------------------
/website/assets/gesture-example-table.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rylanpeng/fern-ar-js/a8c92a1fc2905d1c7578f45767f4d8307ea73efd/website/assets/gesture-example-table.png
--------------------------------------------------------------------------------
/website/gesture-recognition-playground/ascene.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 | AR Scene
15 |
16 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
47 |
48 |
49 |
55 |
56 |
57 |
68 |
69 |
70 |
71 |
--------------------------------------------------------------------------------
/website/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Fern AR
9 |
10 |
11 |
12 |
Fern AR
13 |
14 | Gesture Recognition Playground
15 |
16 | Train Model
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/website/styles.css:
--------------------------------------------------------------------------------
1 | body {
2 | font-family: Arial, sans-serif;
3 | margin: 0;
4 | padding: 0;
5 | background-color: #f4f4f4;
6 | }
7 |
8 | .center-container {
9 | display: flex;
10 | flex-direction: column;
11 | align-items: center;
12 | justify-content: center;
13 | height: 100vh;
14 | }
15 |
16 | button {
17 | padding: 10px;
18 | margin: 10px;
19 | font-size: 16px;
20 | }
21 |
22 | .controls-container {
23 | position: absolute;
24 | display: flex;
25 | flex-direction: column;
26 | align-items: center;
27 | width: 100%;
28 | height: 100%;
29 | }
30 |
31 | .top-right {
32 | position: absolute;
33 | top: 20px;
34 | right: 20px;
35 | }
36 |
37 | .bottom-left {
38 | position: absolute;
39 | bottom: 20px;
40 | left: 20px;
41 | }
42 |
43 | .left-middle {
44 | position: absolute;
45 | top: 50%;
46 | left: 20px;
47 | transform: translateY(-50%);
48 | }
49 |
50 | .bottom-middle {
51 | position: absolute;
52 | bottom: 20px;
53 | left: 50%;
54 | transform: translateX(-50%);
55 | }
56 |
57 | .progress-bar {
58 | width: 50%;
59 | height: 20px;
60 | background-color: #ddd;
61 | margin-top: 10px;
62 | }
--------------------------------------------------------------------------------
/website/train-model/label.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
59 | Label Gestures
60 |
61 |
62 |
67 |
82 |
83 |
84 |
85 |
130 |
131 |
132 |
--------------------------------------------------------------------------------
/website/train-model/label.js:
--------------------------------------------------------------------------------
1 | import {
2 | HandLandmarker,
3 | FilesetResolver,
4 | } from "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0";
5 |
6 | let handLandmarker = undefined;
7 |
8 | const createHandLandmarker = async () => {
9 | const vision = await FilesetResolver.forVisionTasks(
10 | "https://cdn.jsdelivr.net/npm/@mediapipe/tasks-vision@0.10.0/wasm"
11 | );
12 | handLandmarker = await HandLandmarker.createFromOptions(vision, {
13 | baseOptions: {
14 | modelAssetPath: `https://storage.googleapis.com/mediapipe-models/hand_landmarker/hand_landmarker/float16/1/hand_landmarker.task`,
15 | delegate: "GPU",
16 | },
17 | runningMode: "VIDEO",
18 | numHands: 1,
19 | });
20 | };
21 | createHandLandmarker();
22 |
23 | const video = document.getElementById("webcam");
24 | const canvasElement = document.getElementById("output_canvas");
25 | const canvasCtx = canvasElement.getContext("2d");
26 |
27 | document.addEventListener("DOMContentLoaded", function () {
28 | setTimeout(enableCam, 500);
29 | });
30 |
31 | function enableCam(event) {
32 | if (!handLandmarker) {
33 | console.log("Wait! objectDetector not loaded yet.");
34 | return;
35 | }
36 |
37 | const constraints = {
38 | video: true,
39 | };
40 |
41 | navigator.mediaDevices.getUserMedia(constraints).then((stream) => {
42 | video.srcObject = stream;
43 | video.addEventListener("loadeddata", predictWebcam);
44 | });
45 | }
46 |
47 | let lastVideoTime = -1;
48 | let results = undefined;
49 | let storeGestureBool = false;
50 | let csvData = null;
51 |
52 | async function predictWebcam() {
53 | canvasElement.style.width = video.offsetWidth;
54 | canvasElement.style.height = video.offsetHeight;
55 | canvasElement.width = video.offsetWidth;
56 | canvasElement.height = video.offsetHeight;
57 |
58 | let startTimeMs = performance.now();
59 | if (lastVideoTime !== video.currentTime) {
60 | lastVideoTime = video.currentTime;
61 | results = handLandmarker.detectForVideo(video, startTimeMs);
62 | }
63 | canvasCtx.save();
64 | canvasCtx.clearRect(0, 0, canvasElement.width, canvasElement.height);
65 | if (results.landmarks) {
66 | if (results.landmarks.length > 0) {
67 | const preProcessedLandmarkList = preProcessedLandmark(results);
68 | if (preProcessedLandmarkList.length === 42) {
69 | if (storeGestureBool) {
70 | storeGestureBool = false;
71 | let gesture_id = document.getElementById("gesture_id").value;
72 | csvData += gesture_id + "," + preProcessedLandmarkList + "\n";
73 | addMessage("store gesture " + gesture_id);
74 | document.getElementById("messages").innerHTML +=
75 | " store gesture " + gesture_id + " ";
76 | }
77 | }
78 | }
79 |
80 | for (const landmarks of results.landmarks) {
81 | drawConnectors(canvasCtx, landmarks, HAND_CONNECTIONS, {
82 | color: "#C273E5",
83 | lineWidth: 7,
84 | });
85 | drawLandmarks(canvasCtx, landmarks, { color: "#7E369E", lineWidth: 1 });
86 | }
87 | }
88 | canvasCtx.restore();
89 |
90 | // Call this function again to keep predicting when the browser is ready.
91 | window.requestAnimationFrame(predictWebcam);
92 | }
93 |
94 | function preProcessedLandmark(results) {
95 | // calc_landmark_list
96 | const landmark_list = [];
97 | for (const landmarks of results.landmarks) {
98 | for (const landmark of landmarks) {
99 | const landmark_x = Math.min(
100 | Math.floor(landmark.x * canvasElement.width),
101 | canvasElement.width - 1
102 | );
103 | const landmark_y = Math.min(
104 | Math.floor(landmark.y * canvasElement.height),
105 | canvasElement.height - 1
106 | );
107 | landmark_list.push([landmark_x, landmark_y]);
108 | }
109 | }
110 |
111 | // pre_process_landmark
112 | const temp_landmark_list = JSON.parse(JSON.stringify(landmark_list));
113 | let base_x = 0;
114 | let base_y = 0;
115 | for (let index = 0; index < temp_landmark_list.length; index++) {
116 | const landmark_point = temp_landmark_list[index];
117 | if (index === 0) {
118 | base_x = landmark_point[0];
119 | base_y = landmark_point[1];
120 | }
121 | temp_landmark_list[index][0] -= base_x;
122 | temp_landmark_list[index][1] -= base_y;
123 | }
124 | const flatLandmarkList = temp_landmark_list.flat();
125 | const max_value = Math.max(...flatLandmarkList.map(Math.abs));
126 | const normalize_ = (n) => n / max_value;
127 | const normalized_landmark_list = flatLandmarkList.map(normalize_);
128 | return normalized_landmark_list;
129 | }
130 |
131 | window.storeGesture = function storeGesture() {
132 | // TODO: Use mutex?
133 | storeGestureBool = true;
134 | };
135 |
136 | document
137 | .getElementById("training-csv")
138 | .addEventListener("change", function (event) {
139 | const file = event.target.files[0];
140 | if (!file) {
141 | alert("No file selected");
142 | return;
143 | }
144 | const reader = new FileReader();
145 | reader.onload = function (event) {
146 | csvData = event.target.result;
147 | addMessage('CSV file upload successfully');
148 | };
149 | reader.readAsText(file);
150 | });
151 |
152 | window.downloadCsv = function downloadCsv() {
153 | const blob = new Blob([csvData], { type: "text/csv" });
154 | const url = URL.createObjectURL(blob);
155 |
156 | const link = document.createElement("a");
157 | link.href = url;
158 | link.download = "generated-file.csv";
159 |
160 | document.body.appendChild(link);
161 |
162 | link.click();
163 |
164 | document.body.removeChild(link);
165 | URL.revokeObjectURL(url);
166 | };
167 |
168 | window.startTraining = function startTraining() {
169 | const NUM_CLASSES = parseInt(
170 | document.getElementById("trainGestureNum").value
171 | );
172 |
173 | function shuffleArray(array) {
174 | for (let i = array.length - 1; i > 0; i--) {
175 | const j = Math.floor(Math.random() * (i + 1));
176 | [array[i], array[j]] = [array[j], array[i]];
177 | }
178 | return array;
179 | }
180 |
181 | function shuffleData(X_dataset, y_dataset) {
182 | const indices = Array.from({ length: X_dataset.length }, (_, i) => i);
183 | shuffleArray(indices);
184 |
185 | const X_shuffled = indices.map((i) => X_dataset[i]);
186 | const y_shuffled = indices.map((i) => y_dataset[i]);
187 |
188 | return { X: X_shuffled, y: y_shuffled };
189 | }
190 |
191 | async function loadAndPreprocessData() {
192 | const data = csvData
193 | .split("\n")
194 | .slice(0, -1)
195 | .map((row) => row.split(","));
196 | const X_dataset = data.map((row) => row.slice(1).map(parseFloat));
197 | const y_dataset = data.map((row) => [parseInt(row[0])]);
198 |
199 | const shuffledData = shuffleData(X_dataset, y_dataset);
200 |
201 | const splitIndex = Math.floor(0.75 * shuffledData.X.length);
202 | const X_train = tf.tensor2d(shuffledData.X.slice(0, splitIndex));
203 | const X_test = tf.tensor2d(shuffledData.X.slice(splitIndex));
204 | const y_train = tf.tensor2d(shuffledData.y.slice(0, splitIndex));
205 | const y_test = tf.tensor2d(shuffledData.y.slice(splitIndex));
206 | return { X_train, X_test, y_train, y_test };
207 | }
208 |
209 | async function trainModel() {
210 | const progressBar = document.getElementById("progress-bar");
211 | const { X_train, X_test, y_train, y_test } = await loadAndPreprocessData();
212 |
213 | const model = tf.sequential();
214 | model.add(
215 | tf.layers.dense({ inputShape: [21 * 2], units: 20, activation: "relu" })
216 | );
217 | model.add(tf.layers.dropout(0.2));
218 | model.add(tf.layers.dense({ units: 10, activation: "relu" }));
219 | model.add(tf.layers.dropout(0.4));
220 | model.add(tf.layers.dense({ units: NUM_CLASSES, activation: "softmax" }));
221 |
222 | model.compile({
223 | optimizer: "adam",
224 | loss: "sparseCategoricalCrossentropy",
225 | metrics: ["accuracy"],
226 | });
227 |
228 | const epochs = 1000;
229 | const batchSize = 128;
230 |
231 | for (let epoch = 0; epoch < epochs; epoch++) {
232 | const history = await model.fit(X_train, y_train, {
233 | epochs: 1,
234 | batchSize,
235 | validationData: [X_test, y_test],
236 | });
237 |
238 | const progress = ((epoch + 1) / epochs) * 100;
239 | progressBar.style.width = `${progress}%`;
240 |
241 | if (history.history.val_loss[0] < 0.1) {
242 | addMessage("Finished training early.");
243 | break;
244 | }
245 | }
246 | addMessage("Model trained successfully.");
247 |
248 | return model;
249 | }
250 |
251 | async function saveModel() {
252 | document.getElementById("progress-container").style.display = "block";
253 | const model = await trainModel();
254 | await model.save("downloads://model");
255 | document.getElementById("progress-container").style.display = "none";
256 | }
257 | saveModel();
258 | };
259 |
260 | function addMessage(message) {
261 | const messagesContainer = document.getElementById("messages");
262 | const span = document.createElement("span");
263 | span.textContent = message;
264 | messagesContainer.appendChild(span);
265 |
266 | messagesContainer.appendChild(document.createElement("br"));
267 |
268 | const maxMessages = 20;
269 | const messages = messagesContainer.querySelectorAll("span");
270 | if (messages.length > maxMessages) {
271 | messages[0].remove();
272 | messagesContainer.querySelector("br").remove();
273 | }
274 | span.scrollIntoView({ behavior: "smooth", block: "end" });
275 | }
276 |
--------------------------------------------------------------------------------