├── .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 |

title

2 | 3 |

4 |

5 | 6 |

Web Augmented Reality. A-Frame Component. Currently supports gesture recognition

7 | 8 |

9 | 10 | Downloads 11 | 12 | 13 | Version 14 | 15 | 16 | License 17 | 18 | 19 | Jsdelivr 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 | gesture-table 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 | 15 |
16 | 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 |
86 |
87 |

88 |

Step 1: Select local Training CSV File:

89 | 90 |

Step 2: Enter Gesture ID to train

91 | 92 |

93 |

94 | Step 3: Repeat clicking the 'Store Gesture' button while making the 95 | post 96 |

97 |

the web pages will store the gesture into CSV file

98 | 99 |

100 |

101 | Step 4: Repeat doing Step 2 & Step 3 to label as many as you want 102 |

103 | 104 |
105 |
106 |

107 | Step 5: After labeling, you can download the new labeling CSV file or 108 | train the model 109 |

110 | 115 | Don't forget this! This number should be the max gesture ID + 1 116 |

117 | 118 |

119 | 120 |

121 |
122 |
123 |
124 |

125 |
126 |
127 |
128 |
129 |
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 | --------------------------------------------------------------------------------