diff --git a/.flake8 b/.flake8
new file mode 100644
index 0000000..e658540
--- /dev/null
+++ b/.flake8
@@ -0,0 +1,3 @@
+[flake8]
+max-line-length = 120
+extend-ignore = E203, E266, E712, W503
diff --git a/.github/workflows/code_quality.yml b/.github/workflows/code_quality.yml
index 4dcb718..835d21e 100644
--- a/.github/workflows/code_quality.yml
+++ b/.github/workflows/code_quality.yml
@@ -14,10 +14,11 @@ jobs:
uses: actions/setup-python@v3
with:
python-version: ${{ matrix.python-version }}
+ - name: Install uv
+ run: pip install uv
- name: Install dependencies
run: |
- python -m pip install --upgrade pip
- pip install -r requirements_dev.txt
+ uv sync
- name: Analysing code with checkers
run: |
make check
diff --git a/.gitignore b/.gitignore
index bb41848..1c14fc6 100644
--- a/.gitignore
+++ b/.gitignore
@@ -11,4 +11,6 @@ dist/*
*.csv
data/*
.mypy_cache
-.venvs
\ No newline at end of file
+.venvs
+*.task
+/engine/target
diff --git a/Makefile b/Makefile
index bceca88..e772576 100644
--- a/Makefile
+++ b/Makefile
@@ -1,24 +1,60 @@
-.PHONY: format format_check test
+.PHONY: format format_check test min wasm python python-help
-# Detect OS and set PYTHON command accordingly
-ifeq ($(OS),Windows_NT)
- PYTHON := $(shell where python3)
-else
- PYTHON := $(shell which python3)
-endif
+# Python build
+FORMAT_TOOLS = uv run --with isort --with black
+CHECK_TOOLS = uv run --with pylint --with flake8 --with mypy --with isort --with black
+
+MINIFY = npx terser
+MINIFY_FLAGS = --compress --mangle
+JS_SRC = web/src/eyegestures.js
+MIN = $(JS_SRC:.js=.min.js)
format:
- isort eyeGestures
- black eyeGestures
+ $(FORMAT_TOOLS) python -m isort eyeGestures
+ $(FORMAT_TOOLS) python -m black eyeGestures
format_check:
- isort eyeGestures --check
- black eyeGestures --check
+ $(CHECK_TOOLS) python -m isort eyeGestures --check
+ $(CHECK_TOOLS) python -m black eyeGestures --check
check: format_check
- pylint eyeGestures
- flake8 eyeGestures
- mypy eyeGestures
+ $(CHECK_TOOLS) python -m pylint eyeGestures
+ $(CHECK_TOOLS) python -m flake8 eyeGestures
+ $(CHECK_TOOLS) python -m mypy eyeGestures
+
+# Web build
+min:
+ $(MINIFY) $(JS_SRC) $(MINIFY_FLAGS) -o engine/pkg/eyegestures.min.js
+
+wasm:
+ cd ./engine && wasm-pack build --target web
+ $(MINIFY) $(JS_SRC) $(MINIFY_FLAGS) -o engine/pkg/eyegestures.min.js
+
+# Python build
+PYTHON = uv run
+MATURIN_PYTHON_FLAGS = --release --no-default-features --features python
+
+# Set to 1 to allow builds for newer CPython versions than PyO3 officially supports.
+PYO3_USE_ABI3_FORWARD_COMPATIBILITY ?= 1
+PYTHON_INTERPRETERS=python3.12 python3.13
+
+ifeq ($(OS),Windows_NT)
+PYO3_FORWARD_ENV = set PYO3_USE_ABI3_FORWARD_COMPATIBILITY=$(PYO3_USE_ABI3_FORWARD_COMPATIBILITY) &&
+else
+PYO3_FORWARD_ENV = PYO3_USE_ABI3_FORWARD_COMPATIBILITY=$(PYO3_USE_ABI3_FORWARD_COMPATIBILITY)
+endif
+
+ifeq ($(OS),Windows_NT)
+python:
+ $(PYO3_FORWARD_ENV) maturin build $(MATURIN_PYTHON_FLAGS) --target i686-pc-windows-msvc --interpreter $(PYTHON_INTERPRETERS)
+ $(PYO3_FORWARD_ENV) maturin build $(MATURIN_PYTHON_FLAGS) --target x86_64-pc-windows-msvc --interpreter $(PYTHON_INTERPRETERS)
+else
+python:
+ $(PYO3_FORWARD_ENV) maturin build $(MATURIN_PYTHON_FLAGS) --interpreter $(PYTHON_INTERPRETERS)
+endif
+
+clean:
+ rm -f src/eyegestures.min.js
test:
${PYTHON} -m unittest tests/test_*
\ No newline at end of file
diff --git a/README.md b/README.md
index 7b62788..2cd50df 100644
--- a/README.md
+++ b/README.md
@@ -21,12 +21,7 @@ Our [Mission](https://github.com/NativeSensors/EyeGestures/blob/main/MISSION.md)
> [!Warning]
> **Before contributing read the `contributing.md` guide!**
-> [!NOTE]
-> Check JS port: [EyeGesturesLite](https://github.com/NativeSensors/EyeGesturesLite)!
-
-### 💜 Sponsors:
-
-
+### 💜 Sponsors:
> [!IMPORTANT]
> EyeGestures is a fully volunteer-based project and exists thanks to your donations and support.
>
@@ -34,11 +29,7 @@ Our [Mission](https://github.com/NativeSensors/EyeGestures/blob/main/MISSION.md)
>
> 📢📢 **If you are bussiness and would like to collaborate, reach us: contact@eyegestures.com** 📢📢
>
-> We can provide custom comercial license and invoices if needed.
->
->
->
->
+> We are happy to be involved in your project!
>
---------
### 🔨 Projects build with EyeGestures:
@@ -52,23 +43,11 @@ Our [Mission](https://github.com/NativeSensors/EyeGestures/blob/main/MISSION.md)
-- [EyePilot](https://polar.sh/NativeSensors/products/5fce104c-46ec-4203-892b-a26e0e0ead18)
+- [EyePilot](https://polar.sh/NativeSensors/products/5fce104c-46ec-4203-892b-a26e0e0ead18) - waiting to be restored
- [EyePather](https://polar.sh/NativeSensors/posts/eyepather-new-tool-in-eyegestures-ecosystem) - discontinued
-- [EyeFocus](https://polar.sh/NativeSensors/products/3756d557-134e-4e7e-ac50-d850126aa325?ref=producthunt) <- 4th best product on PH!
+- [EyeFocus](https://polar.sh/NativeSensors/products/3756d557-134e-4e7e-ac50-d850126aa325?ref=producthunt)
- Add your project! contact@eyegestures.com or PR
-Subscribe and get access to our software:
-
-
-
---------
-
-
-
-
-### ⚓ Ports:
-- JavaScript version: [EyeGesturesLite](https://github.com/NativeSensors/EyeGesturesLite)!
-
### 💻 Install
```
python3 -m pip install eyeGestures
@@ -82,20 +61,16 @@ python3 -m pip install eyeGestures
Tracker works best when your camera or laptop is at arm's length, similar to how you would typically use it. If you are further away, it may be less responsive for now - currently working on solving this issue.
```
-python3 examples/simple_example_v2.py
-```
-
-```
-python3 examples/simple_example.py [legacy tracker, will become obsolete]
+python3 examples/simple_example_v4.py
```
### 🔧 Build your own:
-#### Using EyeGesture Engine V3 - Faster smaller better:
+#### Engine V4 - Rust based enginee:
```python
from eyeGestures.utils import VideoCapture
-from eyeGestures import EyeGestures_v3
+from eyeGestures import EyeGestures_v4
# Initialize gesture engine and video capture
gestures = EyeGestures_v3()
@@ -123,90 +98,9 @@ while True:
-#### Using EyeGesture Engine V2 - [Legacy]:
-
-```python
-from eyeGestures.utils import VideoCapture
-from eyeGestures import EyeGestures_v2
-
-# Initialize gesture engine and video capture
-gestures = EyeGestures_v2()
-cap = VideoCapture(0)
-calibrate = True
-screen_width = 500
-screen_height= 500
-
-# Process each frame
-while True:
- ret, frame = cap.read()
- event, cevent = gestures.step(frame,
- calibrate,
- screen_width,
- screen_height,
- context="my_context")
-
- if event:
- cursor_x, cursor_y = event.point[0], event.point[1]
- fixation = event.fixation
- # calibration_radius: radius for data collection during calibration
-```
-
-
-
-
-#### Customize [only V1/V2]:
-
-You can customize your calibration points/map to fit your solutions. Simple copy snippet below, and place your calibration poitns on x,y planes from 0.0 to 1.0. It will be then automatically scaled to your display.
-
-```python
-gestures = EyeGestures_v2()
-gestures.uploadCalibrationMap([[0,0],[0,1],[1,0],[1,1]])
-```
-
-V2 is two stage tracker. It runs V1 under the hood but then uses it as feature extractor for V2 machine learning component, and combines both outputs to generate new gaze point. It is possible to control how much V1 affects V2 by:
-
-```python
-gestures.setClassicImpact(N) # setting N = 2 is working best for my testing
-```
-This makes that sample obtained from V2 is averaged with N times sample from V1 (same sample copied that many times). In outcome having V2 impacting output in `1/N+1` and V1 `N/N+1`.
-
-It is also worth to know that you can enable hidden calibration for V1 (same calibration when using only V1, but now it is invisible to user):
-```python
-gestures.enableCNCalib()
-```
-
-#### Using EyeGesture Engine V1 - Model-Based Approach [not recommended]:
-
-```python
-from eyeGestures.utils import VideoCapture
-from eyeGestures import EyeGestures_v1
-
-# Initialize gesture engine with RoI parameters
-gestures = EyeGestures_v1()
-
-cap = VideoCapture(0)
-ret, frame = cap.read()
-calibrate = True
-screen_width = 500
-screen_height= 500
-
-# Obtain estimations from camera frames
-event, cevent = gestures.estimate(
- frame,
- "main",
- calibrate, # set calibration - switch to False to stop calibration
- screen_width,
- screen_height,
- 0, 0, 0.8, 10
-)
-
-if event:
- cursor_x, cursor_y = event.point[0], event.point[1]
- fixation = event.fixation
- # calibration_radius: radius for data collection during calibration
-```
+#### Legacy Enginees:
-Feel free to copy and paste the relevant code snippets for your project.
+With release of V4 rust based engine shared betweeen all eyegestures versions (web and desktop), fully pythonic enginees become deprecated. This decision was driven by trying to achieve single algorithm powering eyegestures application. Keeping multiple different enginees was beyond ability to maintain it. Source code can still be found in `src/Legacy`.
### 🔥 Web Demos:
diff --git a/engine/Cargo.lock b/engine/Cargo.lock
new file mode 100644
index 0000000..2a6d276
--- /dev/null
+++ b/engine/Cargo.lock
@@ -0,0 +1,4101 @@
+# This file is automatically @generated by Cargo.
+# It is not intended for manual editing.
+version = 4
+
+[[package]]
+name = "EyegesturesEngine"
+version = "4.0.0"
+dependencies = [
+ "maturin",
+ "pyo3",
+ "wasm-bindgen",
+ "web-sys",
+]
+
+[[package]]
+name = "adler2"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa"
+
+[[package]]
+name = "aho-corasick"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ddd31a130427c27518df266943a5308ed92d4b226cc639f5a8f1002816174301"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "allocator-api2"
+version = "0.2.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
+
+[[package]]
+name = "anstream"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
+dependencies = [
+ "anstyle",
+ "anstyle-parse",
+ "anstyle-query",
+ "anstyle-wincon",
+ "colorchoice",
+ "is_terminal_polyfill",
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle"
+version = "1.0.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
+
+[[package]]
+name = "anstyle-parse"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
+dependencies = [
+ "utf8parse",
+]
+
+[[package]]
+name = "anstyle-query"
+version = "1.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anstyle-wincon"
+version = "3.0.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
+dependencies = [
+ "anstyle",
+ "once_cell_polyfill",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "anyhow"
+version = "1.0.102"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f202df86484c868dbad7eaa557ef785d5c66295e41b460ef922eca0723b842c"
+
+[[package]]
+name = "ar_archive_writer"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7eb93bbb63b9c227414f6eb3a0adfddca591a8ce1e9b60661bb08969b87e340b"
+dependencies = [
+ "object 0.37.3",
+]
+
+[[package]]
+name = "arwen"
+version = "0.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d44cbd9bd79165abe331ebabb9dd4d59a5dc93791be33ff15ebd71baaadc85ba"
+dependencies = [
+ "clap",
+ "goblin",
+ "object 0.38.1",
+ "scroll",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "arwen-codesign"
+version = "0.0.1-alpha.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "35d7a19757bfe3658d5a95bf25a0492f29ebb21933549bdbfa4075c895510124"
+dependencies = [
+ "goblin",
+ "scroll",
+ "sha2 0.10.9",
+ "tempfile",
+]
+
+[[package]]
+name = "autocfg"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
+
+[[package]]
+name = "base64"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
+
+[[package]]
+name = "bitflags"
+version = "2.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4512299f36f043ab09a583e57bceb5a5aab7a73db1805848e8fef3c9e8c78b3"
+
+[[package]]
+name = "block-buffer"
+version = "0.10.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
+dependencies = [
+ "generic-array",
+]
+
+[[package]]
+name = "block-buffer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cdd35008169921d80bc60d3d0ab416eecb028c4cd653352907921d95084790be"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "borrow-or-share"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
+
+[[package]]
+name = "boxcar"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36f64beae40a84da1b4b26ff2761a5b895c12adc41dc25aaee1c4f2bbfe97a6e"
+
+[[package]]
+name = "bstr"
+version = "1.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63044e1ae8e69f3b5a92c736ca6269b8d12fa7efe39bf34ddb06d102cf0e2cab"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
+name = "bumpalo"
+version = "3.20.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
+
+[[package]]
+name = "byteorder"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
+
+[[package]]
+name = "bytes"
+version = "1.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33"
+
+[[package]]
+name = "bytesize"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6bd91ee7b2422bcb158d90ef4d14f75ef67f340943fc4149891dcce8f8b972a3"
+
+[[package]]
+name = "bzip2"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f3a53fac24f34a81bc9954b5d6cfce0c21e18ec6959f44f56e8e90e4bb7c346c"
+dependencies = [
+ "libbz2-rs-sys",
+]
+
+[[package]]
+name = "cab"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "171228650e6721d5acc0868a462cd864f49ac5f64e4a42cde270406e64e404d2"
+dependencies = [
+ "byteorder",
+ "flate2",
+ "lzxd",
+ "time",
+]
+
+[[package]]
+name = "camino"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e629a66d692cb9ff1a1c664e41771b3dcaf961985a9774c0eb0bd1b51cf60a48"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-config2"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25ada53f7339c78084fb37d7e17f34e76537541c4fbb02fa3a2baa14b8faad37"
+dependencies = [
+ "serde",
+ "serde_derive",
+ "toml 1.1.2+spec-1.1.0",
+]
+
+[[package]]
+name = "cargo-cyclonedx"
+version = "0.5.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d162f67705f0f5038759d73bf546a083bf30e8677c2e944b416bca48d9d69a8"
+dependencies = [
+ "anyhow",
+ "cargo-lock",
+ "cargo_metadata 0.18.1",
+ "clap",
+ "cyclonedx-bom",
+ "env_logger",
+ "log",
+ "once_cell",
+ "pathdiff",
+ "percent-encoding",
+ "purl",
+ "regex",
+ "serde",
+ "thiserror 2.0.18",
+ "time",
+ "validator",
+]
+
+[[package]]
+name = "cargo-lock"
+version = "10.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c06acb4f71407ba205a07cb453211e0e6a67b21904e47f6ba1f9589e38f2e454"
+dependencies = [
+ "semver",
+ "serde",
+ "toml 0.8.23",
+ "url",
+]
+
+[[package]]
+name = "cargo-options"
+version = "0.7.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f89e1d6d6f65fe04d5e21be9de19d31a074e3b7e43aa39ee5b85f4cee16c3188"
+dependencies = [
+ "anstyle",
+ "clap",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35af189006b9c0f00a064685c727031e3ed2d8020f7ba284d78cc2671bd36ea"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "cargo-platform"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "cargo-xwin"
+version = "0.22.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c010a0e90e1dc09a90428c8768808bb4ad8cd7523e4df38719418e14579a9e37"
+dependencies = [
+ "anyhow",
+ "cargo-config2",
+ "cargo-options",
+ "clap",
+ "dirs",
+ "fs-err",
+ "humantime",
+ "indicatif",
+ "paste",
+ "path-slash",
+ "serde",
+ "tar",
+ "tracing-subscriber",
+ "ureq",
+ "which",
+ "xwin",
+ "xz2",
+]
+
+[[package]]
+name = "cargo-zigbuild"
+version = "0.22.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "418b19ee36a550c7c34a0bfd04e0b31d0dfb2c92f54aa36189ea3f2f85764845"
+dependencies = [
+ "anyhow",
+ "cargo-config2",
+ "cargo-options",
+ "cargo_metadata 0.23.1",
+ "clap",
+ "crc",
+ "dirs",
+ "fs-err",
+ "goblin",
+ "path-slash",
+ "rustc_version",
+ "rustflags",
+ "scroll",
+ "semver",
+ "serde",
+ "serde_json",
+ "shell-words",
+ "target-lexicon",
+ "which",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d886547e41f740c616ae73108f6eb70afe6d940c7bc697cb30f13daec073037"
+dependencies = [
+ "camino",
+ "cargo-platform 0.1.9",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 1.0.69",
+]
+
+[[package]]
+name = "cargo_metadata"
+version = "0.23.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef987d17b0a113becdd19d3d0022d04d7ef41f9efe4f3fb63ac44ba61df3ade9"
+dependencies = [
+ "camino",
+ "cargo-platform 0.3.3",
+ "semver",
+ "serde",
+ "serde_json",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "cbindgen"
+version = "0.29.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c95537b45400390270fae69ac098d057c8f5399001cde9d04f700c105ddfff2d"
+dependencies = [
+ "heck",
+ "indexmap",
+ "log",
+ "proc-macro2",
+ "quote",
+ "serde",
+ "serde_json",
+ "syn",
+ "tempfile",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "cc"
+version = "1.2.63"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "556e016178bb5662a08681bbe0f00f8e17631781a4dfc8c45e466e4b185ec27f"
+dependencies = [
+ "find-msvc-tools",
+ "jobserver",
+ "libc",
+ "shlex",
+]
+
+[[package]]
+name = "cesu8"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c"
+
+[[package]]
+name = "cfb"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a347dcabdae9c31b0825fd6a8bed285ec9c2acb89c47827126d52fa4f59cece3"
+dependencies = [
+ "fnv",
+ "uuid",
+ "web-time",
+]
+
+[[package]]
+name = "cfg-if"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
+
+[[package]]
+name = "charset"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1f927b07c74ba84c7e5fe4db2baeb3e996ab2688992e39ac68ce3220a677c7e"
+dependencies = [
+ "base64",
+ "encoding_rs",
+]
+
+[[package]]
+name = "chumsky"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0d2bfadce76f963d776feff99db6dc33783829539258314776383b33e2a00f8"
+dependencies = [
+ "hashbrown 0.15.5",
+ "regex-automata",
+ "serde",
+ "stacker",
+ "unicode-ident",
+ "unicode-segmentation",
+]
+
+[[package]]
+name = "clap"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ddb117e43bbf7dacf0a4190fef4d345b9bad68dfc649cb349e7d17d28428e51"
+dependencies = [
+ "clap_builder",
+ "clap_derive",
+]
+
+[[package]]
+name = "clap_builder"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f"
+dependencies = [
+ "anstream",
+ "anstyle",
+ "clap_lex",
+ "strsim",
+ "terminal_size",
+]
+
+[[package]]
+name = "clap_complete"
+version = "4.6.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e0a7a9bfdb35811f9e59832f0f05975114d2251b415fb534108e6f34060fd772"
+dependencies = [
+ "clap",
+]
+
+[[package]]
+name = "clap_complete_command"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da8e198c052315686d36371e8a3c5778b7852fc75cc313e4e11eeb7a644a1b62"
+dependencies = [
+ "clap",
+ "clap_complete",
+ "clap_complete_nushell",
+]
+
+[[package]]
+name = "clap_complete_nushell"
+version = "4.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fbb9e9715d29a754b468591be588f6b926f5b0a1eb6a8b62acabeb66ff84d897"
+dependencies = [
+ "clap",
+ "clap_complete",
+]
+
+[[package]]
+name = "clap_derive"
+version = "4.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f2ce8604710f6733aa641a2b3731eaa1e8b3d9973d5e3565da11800813f997a9"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "clap_lex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
+
+[[package]]
+name = "cli-table"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "14da8d951cef7cc4f13ccc9b744d736963d57863c7e6fc33c070ea274546082c"
+dependencies = [
+ "termcolor",
+ "unicode-width",
+]
+
+[[package]]
+name = "colorchoice"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
+
+[[package]]
+name = "combine"
+version = "4.6.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd"
+dependencies = [
+ "bytes",
+ "memchr",
+]
+
+[[package]]
+name = "configparser"
+version = "3.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e57e3272f0190c3f1584272d613719ba5fc7df7f4942fe542e63d949cf3a649b"
+
+[[package]]
+name = "console"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d64e8af5551369d19cf50138de61f1c42074ab970f74e99be916646777f8fc87"
+dependencies = [
+ "encode_unicode",
+ "libc",
+ "unicode-width",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "const-oid"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6ef517f0926dd24a1582492c791b6a4818a4d94e789a334894aa15b0d12f55c"
+
+[[package]]
+name = "cookie"
+version = "0.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
+dependencies = [
+ "percent-encoding",
+ "time",
+ "version_check",
+]
+
+[[package]]
+name = "cookie_store"
+version = "0.22.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "15b2c103cf610ec6cae3da84a766285b42fd16aad564758459e6ecf128c75206"
+dependencies = [
+ "cookie",
+ "document-features",
+ "idna",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "time",
+ "url",
+]
+
+[[package]]
+name = "core-foundation"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "core-foundation-sys"
+version = "0.8.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
+
+[[package]]
+name = "cpufeatures"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "cpufeatures"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b2a41393f66f16b0823bb79094d54ac5fbd34ab292ddafb9a0456ac9f87d201"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "crc"
+version = "3.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
+dependencies = [
+ "crc-catalog",
+]
+
+[[package]]
+name = "crc-catalog"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
+
+[[package]]
+name = "crc32fast"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9481c1c90cbf2ac953f07c8d4a58aa3945c425b7185c9154d67a65e4230da511"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "crossbeam-channel"
+version = "0.5.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-deque"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51"
+dependencies = [
+ "crossbeam-epoch",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-epoch"
+version = "0.9.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e"
+dependencies = [
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28"
+
+[[package]]
+name = "crypto-common"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "78c8292055d1c1df0cce5d180393dc8cce0abec0a7102adb6c7b1eef6016d60a"
+dependencies = [
+ "generic-array",
+ "typenum",
+]
+
+[[package]]
+name = "crypto-common"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ce6e4c961d6cd6c9a86db418387425e8bdeaf05b3c8bc1411e6dca4c252f1453"
+dependencies = [
+ "hybrid-array",
+]
+
+[[package]]
+name = "cyclonedx-bom"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3132b69ba8c13808bd2fa5748ac5b9816eb4f4e1f0bff6b7f9254a5940dcdeef"
+dependencies = [
+ "base64",
+ "cyclonedx-bom-macros",
+ "fluent-uri",
+ "indexmap",
+ "once_cell",
+ "ordered-float",
+ "purl",
+ "regex",
+ "serde",
+ "serde_json",
+ "spdx",
+ "strum",
+ "thiserror 2.0.18",
+ "time",
+ "uuid",
+ "xml-rs",
+]
+
+[[package]]
+name = "cyclonedx-bom-macros"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c50341f21df64b412b4f917e34b7aa786c092d64f3f905f478cb76950c7e980c"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "data-encoding"
+version = "2.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8"
+
+[[package]]
+name = "deranged"
+version = "0.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
+dependencies = [
+ "powerfmt",
+]
+
+[[package]]
+name = "dialoguer"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25f104b501bf2364e78d0d3974cbc774f738f5865306ed128e1e0d7499c0ad96"
+dependencies = [
+ "console",
+ "shell-words",
+ "zeroize",
+]
+
+[[package]]
+name = "digest"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
+dependencies = [
+ "block-buffer 0.10.4",
+ "crypto-common 0.1.7",
+]
+
+[[package]]
+name = "digest"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1dd6dbb5841937940781866fa1281a1ff7bd3bf827091440879f9994983d5c2"
+dependencies = [
+ "block-buffer 0.12.0",
+ "const-oid",
+ "crypto-common 0.2.2",
+]
+
+[[package]]
+name = "dirs"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3e8aa94d75141228480295a7d0e7feb620b1a5ad9f12bc40be62411e38cce4e"
+dependencies = [
+ "dirs-sys",
+]
+
+[[package]]
+name = "dirs-sys"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e01a3366d27ee9890022452ee61b2b63a67e6f13f58900b651ff5665f0bb1fab"
+dependencies = [
+ "libc",
+ "option-ext",
+ "redox_users",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "displaydoc"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ac70aa55017e108007fbaf5aa0f54b021c98f92ff8af59d42eda9da96e3dd4f"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "document-features"
+version = "0.2.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d4b8a88685455ed29a21542a33abd9cb6510b6b129abadabdcef0f4c55bc8f61"
+dependencies = [
+ "litrs",
+]
+
+[[package]]
+name = "dunce"
+version = "1.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813"
+
+[[package]]
+name = "either"
+version = "1.16.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e"
+
+[[package]]
+name = "encode_unicode"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34aa73646ffb006b8f5147f3dc182bd4bcb190227ce861fc4a4844bf8e3cb2c0"
+
+[[package]]
+name = "encoding_rs"
+version = "0.8.35"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "env_logger"
+version = "0.10.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4cd405aab171cb85d6735e5c8d9db038c17d3ca007a4d2c25f337935c3d90580"
+dependencies = [
+ "humantime",
+ "is-terminal",
+ "log",
+ "regex",
+ "termcolor",
+]
+
+[[package]]
+name = "equivalent"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+
+[[package]]
+name = "errno"
+version = "0.3.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
+dependencies = [
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "fastrand"
+version = "2.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f1f227452a390804cdb637b74a86990f2a7d7ba4b7d5693aac9b4dd6defd8d6"
+
+[[package]]
+name = "fat-macho"
+version = "0.4.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bed3960cfd332df43d86b2ae31bfed07a7b727a6170ae65dddc342775e810ca7"
+dependencies = [
+ "goblin",
+]
+
+[[package]]
+name = "filetime"
+version = "0.2.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c287a33c7f0a620c38e641e7f60827713987b3c0f26e8ddc9462cc69cf75759"
+dependencies = [
+ "cfg-if",
+ "libc",
+]
+
+[[package]]
+name = "find-msvc-tools"
+version = "0.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
+
+[[package]]
+name = "flate2"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
+dependencies = [
+ "crc32fast",
+ "miniz_oxide",
+ "zlib-rs",
+]
+
+[[package]]
+name = "fluent-uri"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
+dependencies = [
+ "borrow-or-share",
+ "ref-cast",
+ "serde",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "foldhash"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
+
+[[package]]
+name = "foldhash"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "77ce24cb58228fbb8aa041425bb1050850ac19177686ea6e0f41a70416f56fdb"
+
+[[package]]
+name = "form_urlencoded"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
+dependencies = [
+ "percent-encoding",
+]
+
+[[package]]
+name = "fs-err"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "73fde052dbfc920003cfd2c8e2c6e6d4cc7c1091538c3a24226cec0665ab08c0"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "futures-core"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
+
+[[package]]
+name = "futures-task"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
+
+[[package]]
+name = "futures-util"
+version = "0.3.32"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
+dependencies = [
+ "futures-core",
+ "futures-task",
+ "pin-project-lite",
+ "slab",
+]
+
+[[package]]
+name = "generic-array"
+version = "0.14.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a"
+dependencies = [
+ "typenum",
+ "version_check",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 5.3.0",
+ "wasip2",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0de51e6874e94e7bf76d726fc5d13ba782deca734ff60d5bb2fb2607c7406555"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "r-efi 6.0.0",
+ "wasip2",
+ "wasip3",
+]
+
+[[package]]
+name = "glob"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0cc23270f6e1808e30a928bdc84dea0b9b4136a8bc82338574f23baf47bbd280"
+
+[[package]]
+name = "globset"
+version = "0.4.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52dfc19153a48bde0cbd630453615c8151bce3a5adfac7a0aebfbf0a1e1f57e3"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "log",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "goblin"
+version = "0.10.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "17582616a7718cca54cec18e534a76c7c4aec11a8b9a85695712f262fd15a4c8"
+dependencies = [
+ "log",
+ "plain",
+ "scroll",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.15.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
+dependencies = [
+ "allocator-api2",
+ "equivalent",
+ "foldhash 0.1.5",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
+dependencies = [
+ "foldhash 0.2.0",
+]
+
+[[package]]
+name = "hashbrown"
+version = "0.17.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
+
+[[package]]
+name = "heck"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
+
+[[package]]
+name = "hermit-abi"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fc0fef456e4baa96da950455cd02c081ca953b141298e41db3fc7e36b1da849c"
+
+[[package]]
+name = "hex"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
+
+[[package]]
+name = "http"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8be7462df143984c4598a256ef469b251d7d7f9e271135073e78fc535414f3d0"
+dependencies = [
+ "bytes",
+ "itoa",
+]
+
+[[package]]
+name = "httparse"
+version = "1.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
+
+[[package]]
+name = "humantime"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "135b12329e5e3ce057a9f972339ea52bc954fe1e9358ef27f95e89716fbc5424"
+
+[[package]]
+name = "hybrid-array"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9155a582abd142abc056962c29e3ce5ff2ad5469f4246b537ed42c5deba857da"
+dependencies = [
+ "typenum",
+]
+
+[[package]]
+name = "icu_collections"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
+dependencies = [
+ "displaydoc",
+ "potential_utf",
+ "utf8_iter",
+ "yoke",
+ "zerofrom",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_locale_core"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
+dependencies = [
+ "displaydoc",
+ "litemap",
+ "tinystr",
+ "writeable",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
+dependencies = [
+ "icu_collections",
+ "icu_normalizer_data",
+ "icu_properties",
+ "icu_provider",
+ "smallvec",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_normalizer_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
+
+[[package]]
+name = "icu_properties"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
+dependencies = [
+ "icu_collections",
+ "icu_locale_core",
+ "icu_properties_data",
+ "icu_provider",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "icu_properties_data"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
+
+[[package]]
+name = "icu_provider"
+version = "2.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
+dependencies = [
+ "displaydoc",
+ "icu_locale_core",
+ "writeable",
+ "yoke",
+ "zerofrom",
+ "zerotrie",
+ "zerovec",
+]
+
+[[package]]
+name = "id-arena"
+version = "2.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3d3067d79b975e8844ca9eb072e16b31c3c1c36928edf9c6789548c524d0d954"
+
+[[package]]
+name = "idna"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
+dependencies = [
+ "idna_adapter",
+ "smallvec",
+ "utf8_iter",
+]
+
+[[package]]
+name = "idna_adapter"
+version = "1.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
+dependencies = [
+ "icu_normalizer",
+ "icu_properties",
+]
+
+[[package]]
+name = "ignore"
+version = "0.4.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d3d782a365a015e0f5c04902246139249abf769125006fbe7649e2ee88169b4a"
+dependencies = [
+ "crossbeam-deque",
+ "globset",
+ "log",
+ "memchr",
+ "regex-automata",
+ "same-file",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
+name = "indexmap"
+version = "2.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
+dependencies = [
+ "equivalent",
+ "hashbrown 0.17.1",
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "indicatif"
+version = "0.18.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "25470f23803092da7d239834776d653104d551bc4d7eacaf31e6837854b8e9eb"
+dependencies = [
+ "console",
+ "portable-atomic",
+ "unicode-width",
+ "unit-prefix",
+ "web-time",
+]
+
+[[package]]
+name = "is-terminal"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46"
+dependencies = [
+ "hermit-abi",
+ "libc",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "is_terminal_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
+
+[[package]]
+name = "itertools"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "413ee7dfc52ee1a4949ceeb7dbc8a33f2d6c088194d9f922fb8318faf1f01186"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itertools"
+version = "0.14.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2b192c782037fadd9cfa75548310488aabdbf3d2da73885b31bd0abd03351285"
+dependencies = [
+ "either",
+]
+
+[[package]]
+name = "itoa"
+version = "1.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
+
+[[package]]
+name = "jni"
+version = "0.21.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97"
+dependencies = [
+ "cesu8",
+ "cfg-if",
+ "combine",
+ "jni-sys 0.3.1",
+ "log",
+ "thiserror 1.0.69",
+ "walkdir",
+ "windows-sys 0.45.0",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258"
+dependencies = [
+ "jni-sys 0.4.1",
+]
+
+[[package]]
+name = "jni-sys"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c6377a88cb3910bee9b0fa88d4f42e1d2da8e79915598f65fb0c7ee14c878af2"
+dependencies = [
+ "jni-sys-macros",
+]
+
+[[package]]
+name = "jni-sys-macros"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38c0b942f458fe50cdac086d2f946512305e5631e720728f2a61aabcd47a6264"
+dependencies = [
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "jobserver"
+version = "0.1.34"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9afb3de4395d6b3e67a780b6de64b51c978ecf11cb9a462c66be7d4ca9039d33"
+dependencies = [
+ "getrandom 0.3.4",
+ "libc",
+]
+
+[[package]]
+name = "js-sys"
+version = "0.3.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "142bc4740e452c1e57ade0cbc129f139c9093e354346f0872ef985f4f5cf5f11"
+dependencies = [
+ "cfg-if",
+ "futures-util",
+ "once_cell",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "lazy_static"
+version = "1.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
+
+[[package]]
+name = "lddtree"
+version = "0.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39f71c5bd42adf4ee8e6b24da58c1f00a2817d27f618b169399f5adbf9e74492"
+dependencies = [
+ "fs-err",
+ "glob",
+ "goblin",
+ "memmap2",
+]
+
+[[package]]
+name = "leb128fmt"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2"
+
+[[package]]
+name = "libbz2-rs-sys"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "34b357333733e8260735ba5894eb928c02ecc69c78715f01a8019e7fa7f2db4c"
+
+[[package]]
+name = "libc"
+version = "0.2.186"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
+
+[[package]]
+name = "libmimalloc-sys"
+version = "0.1.49"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a45a52f43e1c16f667ccfe4dd8c85b7f7c204fd5e3bf46c5b0db9a5c3c0b8e9"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "libredox"
+version = "0.1.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f02ab6bace2054fb888a3c16f990117b579d14a3088e472d63c6011fa185c9d3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "linux-raw-sys"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53"
+
+[[package]]
+name = "litemap"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
+
+[[package]]
+name = "litrs"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
+
+[[package]]
+name = "lock_api"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
+dependencies = [
+ "scopeguard",
+]
+
+[[package]]
+name = "log"
+version = "0.4.30"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "616ec5685824bcc94416c6d4a7a446eea774a31efd7062c8480ba6fd06d7a6e5"
+
+[[package]]
+name = "lzma-rust2"
+version = "0.16.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5e9ceaec84b54518262de7cf06b8b43e83c808349960f1610b21b0bfc9640f20"
+dependencies = [
+ "sha2 0.11.0",
+]
+
+[[package]]
+name = "lzma-sys"
+version = "0.1.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5fda04ab3764e6cde78b9974eec4f779acaba7c4e84b36eca3cf77c581b85d27"
+dependencies = [
+ "cc",
+ "libc",
+ "pkg-config",
+]
+
+[[package]]
+name = "lzxd"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b29dffab797218e12e4df08ef5d15ab9efca2504038b1b32b9b32fc844b39c9"
+
+[[package]]
+name = "mailparse"
+version = "0.16.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60819a97ddcb831a5614eb3b0174f3620e793e97e09195a395bfa948fd68ed2f"
+dependencies = [
+ "charset",
+ "data-encoding",
+ "quoted_printable",
+]
+
+[[package]]
+name = "matchers"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
+dependencies = [
+ "regex-automata",
+]
+
+[[package]]
+name = "maturin"
+version = "1.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8b4e123c32db7b76ff7ad0d3b5c9795ef9d4f013cb75bc0baaa5c17a70995ed3"
+dependencies = [
+ "anyhow",
+ "arwen",
+ "arwen-codesign",
+ "base64",
+ "bytesize",
+ "cargo-config2",
+ "cargo-cyclonedx",
+ "cargo-options",
+ "cargo-xwin",
+ "cargo-zigbuild",
+ "cargo_metadata 0.23.1",
+ "cbindgen",
+ "cc",
+ "clap",
+ "clap_complete_command",
+ "configparser",
+ "console",
+ "dialoguer",
+ "dirs",
+ "dunce",
+ "fat-macho",
+ "flate2",
+ "fs-err",
+ "glob",
+ "goblin",
+ "ignore",
+ "indexmap",
+ "itertools 0.14.0",
+ "lddtree",
+ "memmap2",
+ "minijinja",
+ "normpath",
+ "once_cell",
+ "path-slash",
+ "pep440_rs",
+ "pep508_rs",
+ "platform-info",
+ "pyo3-introspection",
+ "pyproject-toml",
+ "python-pkginfo",
+ "reflink-copy",
+ "regex",
+ "rustc_version",
+ "rustflags",
+ "rustls",
+ "rustls-pki-types",
+ "same-file",
+ "semver",
+ "serde",
+ "serde_json",
+ "sha2 0.11.0",
+ "tar",
+ "target-lexicon",
+ "tempfile",
+ "textwrap",
+ "thiserror 2.0.18",
+ "time",
+ "toml 1.1.2+spec-1.1.0",
+ "toml_edit 0.25.12+spec-1.1.0",
+ "tracing",
+ "tracing-subscriber",
+ "unicode-xid",
+ "ureq",
+ "url",
+ "walkdir",
+ "which",
+ "wild",
+ "xz2",
+ "zip 8.6.0",
+]
+
+[[package]]
+name = "memchr"
+version = "2.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
+
+[[package]]
+name = "memmap2"
+version = "0.9.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "714098028fe011992e1c3962653c96b2d578c4b4bce9036e15ff220319b1e0e3"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "memo-map"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "38d1115007560874e373613744c6fba374c17688327a71c1476d1a5954cc857b"
+
+[[package]]
+name = "mimalloc"
+version = "0.1.52"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d4139bb28d14ad1facf21d5eb8825051b326e172d216b39f6d31df53cc97862"
+dependencies = [
+ "libmimalloc-sys",
+]
+
+[[package]]
+name = "mime"
+version = "0.3.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
+
+[[package]]
+name = "mime_guess"
+version = "2.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e"
+dependencies = [
+ "mime",
+ "unicase",
+]
+
+[[package]]
+name = "minijinja"
+version = "2.20.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2929e494b2280e1e18959bb2e121da03347ae896896fdfaceaab43c88a02803f"
+dependencies = [
+ "memo-map",
+ "serde",
+]
+
+[[package]]
+name = "miniz_oxide"
+version = "0.8.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fa76a2c86f704bdb222d66965fb3d63269ce38518b83cb0575fca855ebb6316"
+dependencies = [
+ "adler2",
+ "simd-adler32",
+]
+
+[[package]]
+name = "msi"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b0325f8473ef1f5c38ee42345e2cd1678299cbbfa169d1776654a2a682867420"
+dependencies = [
+ "byteorder",
+ "cfb",
+ "encoding_rs",
+ "uuid",
+]
+
+[[package]]
+name = "nom"
+version = "8.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df9761775871bdef83bee530e60050f7e54b1105350d6884eb0fb4f46c2f9405"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "normpath"
+version = "1.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9985ef7269fa99f3b12437bb698381da2428743ab90f20393f399fa14cab21a"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "nu-ansi-term"
+version = "0.50.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "num-conv"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
+
+[[package]]
+name = "num-traits"
+version = "0.2.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
+dependencies = [
+ "autocfg",
+]
+
+[[package]]
+name = "object"
+version = "0.37.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff76201f031d8863c38aa7f905eca4f53abbfa15f609db4277d44cd8938f33fe"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "object"
+version = "0.38.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "271638cd5fa9cca89c4c304675ca658efc4e64a66c716b7cfe1afb4b9611dbbc"
+dependencies = [
+ "crc32fast",
+ "flate2",
+ "hashbrown 0.16.1",
+ "indexmap",
+ "memchr",
+ "ruzstd",
+]
+
+[[package]]
+name = "once_cell"
+version = "1.21.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
+
+[[package]]
+name = "once_cell_polyfill"
+version = "1.70.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
+
+[[package]]
+name = "openssl-probe"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe"
+
+[[package]]
+name = "option-ext"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
+
+[[package]]
+name = "ordered-float"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7d950ca161dc355eaf28f82b11345ed76c6e1f6eb1f4f4479e0323b9e2fbd0e"
+dependencies = [
+ "num-traits",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
+dependencies = [
+ "lock_api",
+ "parking_lot_core",
+]
+
+[[package]]
+name = "parking_lot_core"
+version = "0.9.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-link",
+]
+
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
+[[package]]
+name = "path-slash"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42"
+
+[[package]]
+name = "pathdiff"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3"
+dependencies = [
+ "camino",
+]
+
+[[package]]
+name = "pep440_rs"
+version = "0.7.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31095ca1f396e3de32745f42b20deef7bc09077f918b085307e8eab6ddd8fb9c"
+dependencies = [
+ "once_cell",
+ "serde",
+ "tracing",
+ "unicode-width",
+ "unscanny",
+ "version-ranges",
+]
+
+[[package]]
+name = "pep508_rs"
+version = "0.9.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "faee7227064121fcadcd2ff788ea26f0d8f2bd23a0574da11eca23bc935bcc05"
+dependencies = [
+ "boxcar",
+ "indexmap",
+ "itertools 0.13.0",
+ "once_cell",
+ "pep440_rs",
+ "regex",
+ "rustc-hash",
+ "serde",
+ "smallvec",
+ "thiserror 1.0.69",
+ "tracing",
+ "unicode-width",
+ "url",
+ "urlencoding",
+ "version-ranges",
+]
+
+[[package]]
+name = "percent-encoding"
+version = "2.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
+
+[[package]]
+name = "phf"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f84ac04429c13a7ff43785d75ad27569f2951ce0ffd30a3321230db2fc727216"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "unicase",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
+dependencies = [
+ "siphasher",
+ "unicase",
+]
+
+[[package]]
+name = "pin-project-lite"
+version = "0.2.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
+
+[[package]]
+name = "pkg-config"
+version = "0.3.33"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
+
+[[package]]
+name = "plain"
+version = "0.2.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
+
+[[package]]
+name = "platform-info"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9368d62437c8cbb7c31ee37fd8c08a7d390e09a3ff75698a674953f46705ffcb"
+dependencies = [
+ "libc",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "portable-atomic"
+version = "1.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49"
+
+[[package]]
+name = "potential_utf"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
+dependencies = [
+ "zerovec",
+]
+
+[[package]]
+name = "powerfmt"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
+
+[[package]]
+name = "prettyplease"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b"
+dependencies = [
+ "proc-macro2",
+ "syn",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.106"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "psm"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "645dbe486e346d9b5de3ef16ede18c26e6c70ad97418f4874b8b1889d6e761ea"
+dependencies = [
+ "ar_archive_writer",
+ "cc",
+]
+
+[[package]]
+name = "purl"
+version = "0.1.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "60ebe4262ae91ddd28c8721111a0a6e9e58860e211fc92116c4bb85c98fd96ad"
+dependencies = [
+ "hex",
+ "percent-encoding",
+ "phf",
+ "thiserror 2.0.18",
+ "unicase",
+]
+
+[[package]]
+name = "pyo3"
+version = "0.28.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91fd8e38a3b50ed1167fb981cd6fd60147e091784c427b8f7183a7ee32c31c12"
+dependencies = [
+ "libc",
+ "once_cell",
+ "portable-atomic",
+ "pyo3-build-config",
+ "pyo3-ffi",
+ "pyo3-macros",
+]
+
+[[package]]
+name = "pyo3-build-config"
+version = "0.28.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e368e7ddfdeb98c9bca7f8383be1648fd84ab466bf2bc015e94008db6d35611e"
+dependencies = [
+ "python3-dll-a",
+ "target-lexicon",
+]
+
+[[package]]
+name = "pyo3-ffi"
+version = "0.28.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7f29e10af80b1f7ccaf7f69eace800a03ecd13e883acfacc1e5d0988605f651e"
+dependencies = [
+ "libc",
+ "pyo3-build-config",
+]
+
+[[package]]
+name = "pyo3-introspection"
+version = "0.28.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96040524552ac54e645ce08146b24023d720ceb0e788fff758c0beb9fe841736"
+dependencies = [
+ "anyhow",
+ "goblin",
+ "serde",
+ "serde_json",
+]
+
+[[package]]
+name = "pyo3-macros"
+version = "0.28.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df6e520eff47c45997d2fc7dd8214b25dd1310918bbb2642156ef66a67f29813"
+dependencies = [
+ "proc-macro2",
+ "pyo3-macros-backend",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyo3-macros-backend"
+version = "0.28.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c4cdc218d835738f81c2338f822078af45b4afdf8b2e33cbb5916f108b813acb"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "pyo3-build-config",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "pyproject-toml"
+version = "0.13.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6d755483ad14b49e76713b52285235461a5b4f73f17612353e11a5de36a5fd2"
+dependencies = [
+ "glob",
+ "indexmap",
+ "pep440_rs",
+ "pep508_rs",
+ "serde",
+ "thiserror 2.0.18",
+ "toml 0.9.12+spec-1.1.0",
+]
+
+[[package]]
+name = "python-pkginfo"
+version = "0.6.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "229fe47647d6602b9b0934b21fab8aece1c5a5aeb0a934196a14355fec656623"
+dependencies = [
+ "flate2",
+ "fs-err",
+ "mailparse",
+ "rfc2047-decoder",
+ "tar",
+ "thiserror 2.0.18",
+ "zip 8.6.0",
+]
+
+[[package]]
+name = "python3-dll-a"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d80ba7540edb18890d444c5aa8e1f1f99b1bdf26fb26ae383135325f4a36042b"
+dependencies = [
+ "cc",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.45"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
+dependencies = [
+ "proc-macro2",
+]
+
+[[package]]
+name = "quoted_printable"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "478e0585659a122aa407eb7e3c0e1fa51b1d8a870038bd29f0cf4a8551eea972"
+
+[[package]]
+name = "r-efi"
+version = "5.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "69cdb34c158ceb288df11e18b4bd39de994f6657d83847bdffdbd7f346754b0f"
+
+[[package]]
+name = "r-efi"
+version = "6.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
+
+[[package]]
+name = "rand"
+version = "0.8.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a"
+dependencies = [
+ "rand_core",
+]
+
+[[package]]
+name = "rand_core"
+version = "0.6.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
+
+[[package]]
+name = "rayon"
+version = "1.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fb39b166781f92d482534ef4b4b1b2568f42613b53e5b6c160e24cfbfa30926d"
+dependencies = [
+ "either",
+ "rayon-core",
+]
+
+[[package]]
+name = "rayon-core"
+version = "1.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22e18b0f0062d30d4230b2e85ff77fdfe4326feb054b9783a3460d8435c8ab91"
+dependencies = [
+ "crossbeam-deque",
+ "crossbeam-utils",
+]
+
+[[package]]
+name = "redox_syscall"
+version = "0.5.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "redox_users"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4e608c6638b9c18977b00b475ac1f28d14e84b27d8d42f70e0bf1e3dec127ac"
+dependencies = [
+ "getrandom 0.2.17",
+ "libredox",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ref-cast"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f354300ae66f76f1c85c5f84693f0ce81d747e2c3f21a45fef496d89c960bf7d"
+dependencies = [
+ "ref-cast-impl",
+]
+
+[[package]]
+name = "ref-cast-impl"
+version = "1.0.25"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "reflink-copy"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13362233b147e57674c37b802d216b7c5e3dcccbed8967c84f0d8d223868ae27"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "rustix",
+ "windows",
+]
+
+[[package]]
+name = "regex"
+version = "1.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e10754a14b9137dd7b1e3e5b0493cc9171fdd105e0ab477f51b72e7f3ac0e276"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-automata",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-automata"
+version = "0.4.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.8.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a"
+
+[[package]]
+name = "rfc2047-decoder"
+version = "1.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b36447d5e70933adee73cefff22b0851edbc162f2b3951918de43717a22d92de"
+dependencies = [
+ "base64",
+ "charset",
+ "chumsky",
+ "memchr",
+ "quoted_printable",
+ "thiserror 2.0.18",
+]
+
+[[package]]
+name = "ring"
+version = "0.17.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "getrandom 0.2.17",
+ "libc",
+ "untrusted",
+ "windows-sys 0.52.0",
+]
+
+[[package]]
+name = "rustc-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94300abf3f1ae2e2b8ffb7b58043de3d399c73fa6f4b73826402a5c457614dbe"
+
+[[package]]
+name = "rustc_version"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
+dependencies = [
+ "semver",
+]
+
+[[package]]
+name = "rustflags"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a39e0e9135d7a7208ee80aa4e3e4b88f0f5ad7be92153ed70686c38a03db2e63"
+
+[[package]]
+name = "rustix"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190"
+dependencies = [
+ "bitflags",
+ "errno",
+ "libc",
+ "linux-raw-sys",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls"
+version = "0.23.40"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ef86cd5876211988985292b91c96a8f2d298df24e75989a43a3c73f2d4d8168b"
+dependencies = [
+ "log",
+ "once_cell",
+ "ring",
+ "rustls-pki-types",
+ "rustls-webpki",
+ "subtle",
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-native-certs"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63"
+dependencies = [
+ "openssl-probe",
+ "rustls-pki-types",
+ "schannel",
+ "security-framework",
+]
+
+[[package]]
+name = "rustls-pki-types"
+version = "1.14.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9"
+dependencies = [
+ "zeroize",
+]
+
+[[package]]
+name = "rustls-platform-verifier"
+version = "0.6.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784"
+dependencies = [
+ "core-foundation",
+ "core-foundation-sys",
+ "jni",
+ "log",
+ "once_cell",
+ "rustls",
+ "rustls-native-certs",
+ "rustls-platform-verifier-android",
+ "rustls-webpki",
+ "security-framework",
+ "security-framework-sys",
+ "webpki-root-certs",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "rustls-platform-verifier-android"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f"
+
+[[package]]
+name = "rustls-webpki"
+version = "0.103.13"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
+dependencies = [
+ "ring",
+ "rustls-pki-types",
+ "untrusted",
+]
+
+[[package]]
+name = "rustversion"
+version = "1.0.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d"
+
+[[package]]
+name = "ruzstd"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7c1c839d570d835527c9a5e4db7cb2198683a988cb9d7293fc8674e6bd58fc8"
+dependencies = [
+ "twox-hash",
+]
+
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "schannel"
+version = "0.1.29"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91c1b7e4904c873ef0710c1f407dde2e6287de2bebc1bbbf7d430bb7cbffd939"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "scopeguard"
+version = "1.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
+
+[[package]]
+name = "scroll"
+version = "0.13.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1257cd4248b4132760d6524d6dda4e053bc648c9070b960929bf50cfb1e7add"
+dependencies = [
+ "scroll_derive",
+]
+
+[[package]]
+name = "scroll_derive"
+version = "0.13.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed76efe62313ab6610570951494bdaa81568026e0318eaa55f167de70eeea67d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "security-framework"
+version = "3.7.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7f4bc775c73d9a02cde8bf7b2ec4c9d12743edf609006c7facc23998404cd1d"
+dependencies = [
+ "bitflags",
+ "core-foundation",
+ "core-foundation-sys",
+ "libc",
+ "security-framework-sys",
+]
+
+[[package]]
+name = "security-framework-sys"
+version = "2.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3"
+dependencies = [
+ "core-foundation-sys",
+ "libc",
+]
+
+[[package]]
+name = "semver"
+version = "1.0.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
+dependencies = [
+ "serde",
+ "serde_core",
+]
+
+[[package]]
+name = "serde"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
+dependencies = [
+ "serde_core",
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_core"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.228"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "serde_json"
+version = "1.0.150"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
+dependencies = [
+ "itoa",
+ "memchr",
+ "serde",
+ "serde_core",
+ "zmij",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "0.6.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bf41e0cfaf7226dca15e8197172c295a782857fcb97fad1808a166870dee75a3"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "serde_spanned"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "sha2"
+version = "0.10.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.2.17",
+ "digest 0.10.7",
+]
+
+[[package]]
+name = "sha2"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "446ba717509524cb3f22f17ecc096f10f4822d76ab5c0b9822c5f9c284e825f4"
+dependencies = [
+ "cfg-if",
+ "cpufeatures 0.3.0",
+ "digest 0.11.3",
+]
+
+[[package]]
+name = "sharded-slab"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
+dependencies = [
+ "lazy_static",
+]
+
+[[package]]
+name = "shell-words"
+version = "1.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc6fe69c597f9c37bfeeeeeb33da3530379845f10be461a66d16d03eca2ded77"
+
+[[package]]
+name = "shlex"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
+
+[[package]]
+name = "simd-adler32"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214"
+
+[[package]]
+name = "siphasher"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
+
+[[package]]
+name = "slab"
+version = "0.4.12"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
+
+[[package]]
+name = "smallvec"
+version = "1.15.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
+
+[[package]]
+name = "smawk"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c388c1b5e93756d0c740965c41e8822f866621d41acbdf6336a6a168f8840c"
+
+[[package]]
+name = "socks"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0c3dbbd9ae980613c6dd8e28a9407b50509d3803b57624d5dfe8315218cd58b"
+dependencies = [
+ "byteorder",
+ "libc",
+ "winapi",
+]
+
+[[package]]
+name = "spdx"
+version = "0.13.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8da593e30beb790fc9424502eb898320b44e5eb30367dbda1c1edde8e2f32d7"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "stable_deref_trait"
+version = "1.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
+
+[[package]]
+name = "stacker"
+version = "0.1.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "640c8cdd92b6b12f5bcb1803ca3bbf5ab96e5e6b6b96b9ab77dabe9e880b3190"
+dependencies = [
+ "cc",
+ "cfg-if",
+ "libc",
+ "psm",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "strsim"
+version = "0.11.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
+
+[[package]]
+name = "strum"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9628de9b8791db39ceda2b119bbe13134770b56c138ec1d3af810d045c04f9bd"
+dependencies = [
+ "strum_macros",
+]
+
+[[package]]
+name = "strum_macros"
+version = "0.28.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab85eea0270ee17587ed4156089e10b9e6880ee688791d45a905f5b1ca36f664"
+dependencies = [
+ "heck",
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "subtle"
+version = "2.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
+
+[[package]]
+name = "syn"
+version = "2.0.117"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "unicode-ident",
+]
+
+[[package]]
+name = "synstructure"
+version = "0.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tar"
+version = "0.4.46"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f6221d9a6003c78398e3b239969f352578258df48c8eb051caadae0015bc840"
+dependencies = [
+ "filetime",
+ "libc",
+ "xattr",
+]
+
+[[package]]
+name = "target-lexicon"
+version = "0.13.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
+
+[[package]]
+name = "tempfile"
+version = "3.27.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd"
+dependencies = [
+ "fastrand",
+ "getrandom 0.4.2",
+ "once_cell",
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "termcolor"
+version = "1.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
+dependencies = [
+ "winapi-util",
+]
+
+[[package]]
+name = "terminal_size"
+version = "0.4.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "230a1b821ccbd75b185820a1f1ff7b14d21da1e442e22c0863ea5f08771a8874"
+dependencies = [
+ "rustix",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.16.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057"
+dependencies = [
+ "smawk",
+ "unicode-linebreak",
+ "unicode-width",
+]
+
+[[package]]
+name = "thiserror"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
+dependencies = [
+ "thiserror-impl 1.0.69",
+]
+
+[[package]]
+name = "thiserror"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4288b5bcbc7920c07a1149a35cf9590a2aa808e0bc1eafaade0b80947865fbc4"
+dependencies = [
+ "thiserror-impl 2.0.18",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "1.0.69"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thiserror-impl"
+version = "2.0.18"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f60246a4944f24f6e018aa17cdeffb7818b76356965d03b07d6a9886e8962185"
+dependencies = [
+ "cfg-if",
+]
+
+[[package]]
+name = "time"
+version = "0.3.47"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "743bd48c283afc0388f9b8827b976905fb217ad9e647fae3a379a9283c4def2c"
+dependencies = [
+ "deranged",
+ "itoa",
+ "js-sys",
+ "num-conv",
+ "powerfmt",
+ "serde_core",
+ "time-core",
+ "time-macros",
+]
+
+[[package]]
+name = "time-core"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca"
+
+[[package]]
+name = "time-macros"
+version = "0.2.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e70e4c5a0e0a8a4823ad65dfe1a6930e4f4d756dcd9dd7939022b5e8c501215"
+dependencies = [
+ "num-conv",
+ "time-core",
+]
+
+[[package]]
+name = "tinystr"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
+dependencies = [
+ "displaydoc",
+ "zerovec",
+]
+
+[[package]]
+name = "toml"
+version = "0.8.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362"
+dependencies = [
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
+ "toml_edit 0.22.27",
+]
+
+[[package]]
+name = "toml"
+version = "0.9.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 0.7.5+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
+dependencies = [
+ "indexmap",
+ "serde_core",
+ "serde_spanned 1.1.1",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.3",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.6.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "22cddaf88f4fbc13c51aebbf5f8eceb5c7c5a9da2ac40a13519eb5b0a0e8f11c"
+dependencies = [
+ "serde",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "0.7.5+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_datetime"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
+dependencies = [
+ "serde_core",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.22.27"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a"
+dependencies = [
+ "indexmap",
+ "serde",
+ "serde_spanned 0.6.9",
+ "toml_datetime 0.6.11",
+ "toml_write",
+ "winnow 0.7.15",
+]
+
+[[package]]
+name = "toml_edit"
+version = "0.25.12+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
+dependencies = [
+ "indexmap",
+ "toml_datetime 1.1.1+spec-1.1.0",
+ "toml_parser",
+ "toml_writer",
+ "winnow 1.0.3",
+]
+
+[[package]]
+name = "toml_parser"
+version = "1.1.2+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
+dependencies = [
+ "winnow 1.0.3",
+]
+
+[[package]]
+name = "toml_write"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801"
+
+[[package]]
+name = "toml_writer"
+version = "1.1.1+spec-1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
+
+[[package]]
+name = "tracing"
+version = "0.1.44"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
+dependencies = [
+ "pin-project-lite",
+ "tracing-attributes",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-attributes"
+version = "0.1.31"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "tracing-core"
+version = "0.1.36"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
+dependencies = [
+ "once_cell",
+ "valuable",
+]
+
+[[package]]
+name = "tracing-log"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
+dependencies = [
+ "log",
+ "once_cell",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-serde"
+version = "0.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "704b1aeb7be0d0a84fc9828cae51dab5970fee5088f83d1dd7ee6f6246fc6ff1"
+dependencies = [
+ "serde",
+ "tracing-core",
+]
+
+[[package]]
+name = "tracing-subscriber"
+version = "0.3.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
+dependencies = [
+ "matchers",
+ "nu-ansi-term",
+ "once_cell",
+ "regex-automata",
+ "serde",
+ "serde_json",
+ "sharded-slab",
+ "smallvec",
+ "thread_local",
+ "tracing",
+ "tracing-core",
+ "tracing-log",
+ "tracing-serde",
+]
+
+[[package]]
+name = "twox-hash"
+version = "2.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9ea3136b675547379c4bd395ca6b938e5ad3c3d20fad76e7fe85f9e0d011419c"
+
+[[package]]
+name = "typed-path"
+version = "0.12.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e28f89b80c87b8fb0cf04ab448d5dd0dd0ade2f8891bae878de66a75a28600e"
+
+[[package]]
+name = "typenum"
+version = "1.20.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
+
+[[package]]
+name = "unicase"
+version = "2.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbc4bc3a9f746d862c45cb89d705aa10f187bb96c76001afab07a0d35ce60142"
+
+[[package]]
+name = "unicode-ident"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
+
+[[package]]
+name = "unicode-linebreak"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f"
+
+[[package]]
+name = "unicode-segmentation"
+version = "1.13.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9629274872b2bfaf8d66f5f15725007f635594914870f65218920345aa11aa8c"
+
+[[package]]
+name = "unicode-width"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b4ac048d71ede7ee76d585517add45da530660ef4390e49b098733c6e897f254"
+
+[[package]]
+name = "unicode-xid"
+version = "0.2.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853"
+
+[[package]]
+name = "unit-prefix"
+version = "0.5.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81e544489bf3d8ef66c953931f56617f423cd4b5494be343d9b9d3dda037b9a3"
+
+[[package]]
+name = "unscanny"
+version = "0.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e9df2af067a7953e9c3831320f35c1cc0600c30d44d9f7a12b01db1cd88d6b47"
+
+[[package]]
+name = "untrusted"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
+
+[[package]]
+name = "ureq"
+version = "3.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dea7109cdcd5864d4eeb1b58a1648dc9bf520360d7af16ec26d0a9354bafcfc0"
+dependencies = [
+ "base64",
+ "cookie_store",
+ "flate2",
+ "getrandom 0.4.2",
+ "log",
+ "mime_guess",
+ "percent-encoding",
+ "rustls",
+ "rustls-pki-types",
+ "rustls-platform-verifier",
+ "serde",
+ "serde_json",
+ "socks",
+ "ureq-proto",
+ "utf8-zero",
+ "webpki-roots",
+]
+
+[[package]]
+name = "ureq-proto"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e994ba84b0bd1b1b0cf92878b7ef898a5c1760108fe7b6010327e274917a808c"
+dependencies = [
+ "base64",
+ "http",
+ "httparse",
+ "log",
+]
+
+[[package]]
+name = "url"
+version = "2.5.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
+dependencies = [
+ "form_urlencoded",
+ "idna",
+ "percent-encoding",
+ "serde",
+ "serde_derive",
+]
+
+[[package]]
+name = "urlencoding"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da"
+
+[[package]]
+name = "utf8-zero"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8c0a043c9540bae7c578c88f91dda8bd82e59ae27c21baca69c8b191aaf5a6e"
+
+[[package]]
+name = "utf8_iter"
+version = "1.0.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
+
+[[package]]
+name = "utf8parse"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
+
+[[package]]
+name = "uuid"
+version = "1.23.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d258b83ceec21034727ecee8c382cfa6c3e133699b0742c64571814fb420c9f7"
+dependencies = [
+ "getrandom 0.4.2",
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "validator"
+version = "0.19.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d0b4a29d8709210980a09379f27ee31549b73292c87ab9899beee1c0d3be6303"
+dependencies = [
+ "idna",
+ "once_cell",
+ "regex",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "url",
+]
+
+[[package]]
+name = "valuable"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
+
+[[package]]
+name = "version-ranges"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31e9bd4e9c9ff6a2a9b5969462ba26216af3e010df0377dad8320ab515262ef8"
+dependencies = [
+ "smallvec",
+]
+
+[[package]]
+name = "version_check"
+version = "0.9.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
+
+[[package]]
+name = "versions"
+version = "7.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80a7e511ce1795821207a837b7b1c8d8aca0c648810966ad200446ae58f6667f"
+dependencies = [
+ "itertools 0.14.0",
+ "nom",
+]
+
+[[package]]
+name = "walkdir"
+version = "2.5.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
+name = "wasi"
+version = "0.11.1+wasi-snapshot-preview1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
+
+[[package]]
+name = "wasip2"
+version = "1.0.3+wasi-0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6"
+dependencies = [
+ "wit-bindgen 0.57.1",
+]
+
+[[package]]
+name = "wasip3"
+version = "0.4.0+wasi-0.3.0-rc-2026-01-06"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5"
+dependencies = [
+ "wit-bindgen 0.51.0",
+]
+
+[[package]]
+name = "wasm-bindgen"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ed04576f974d2b2fba0f38c51dbc5518011e38c36bf1143164be765528fd409"
+dependencies = [
+ "cfg-if",
+ "once_cell",
+ "rustversion",
+ "wasm-bindgen-macro",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-macro"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "916151b09da36bd82f6615cbf3a419e2f0ba23a03c6160e8e92eb6bd4aa1dec6"
+dependencies = [
+ "quote",
+ "wasm-bindgen-macro-support",
+]
+
+[[package]]
+name = "wasm-bindgen-macro-support"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "299047362ccbfce148b67ab7e73349f77748e00c8296f9542adfad2ad82c5c5e"
+dependencies = [
+ "bumpalo",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wasm-bindgen-shared",
+]
+
+[[package]]
+name = "wasm-bindgen-shared"
+version = "0.2.122"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9a929b2c61f11ba3e9bc35b50c1f25cb38e0e892c0c231ae2b8cf78d5dad4437"
+dependencies = [
+ "unicode-ident",
+]
+
+[[package]]
+name = "wasm-encoder"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319"
+dependencies = [
+ "leb128fmt",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasm-metadata"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909"
+dependencies = [
+ "anyhow",
+ "indexmap",
+ "wasm-encoder",
+ "wasmparser",
+]
+
+[[package]]
+name = "wasmparser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe"
+dependencies = [
+ "bitflags",
+ "hashbrown 0.15.5",
+ "indexmap",
+ "semver",
+]
+
+[[package]]
+name = "web-sys"
+version = "0.3.99"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6d621441cfc37b84979402712047321980c178f299193a3589d05b99e8763436"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "web-time"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5a6580f308b1fad9207618087a65c04e7a10bc77e02c8e84e9b00dd4b12fa0bb"
+dependencies = [
+ "js-sys",
+ "wasm-bindgen",
+]
+
+[[package]]
+name = "webpki-root-certs"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31141ce3fc3e300ae89b78c0dd67f9708061d1d2eda54b8209346fd6be9a92c"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "webpki-roots"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52f5ee44c96cf55f1b349600768e3ece3a8f26010c05265ab73f945bb1a2eb9d"
+dependencies = [
+ "rustls-pki-types",
+]
+
+[[package]]
+name = "which"
+version = "8.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "81995fafaaaf6ae47a7d0cc83c67caf92aeb7e5331650ae6ff856f7c0c60c459"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "wild"
+version = "2.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a3131afc8c575281e1e80f36ed6a092aa502c08b18ed7524e86fbbb12bb410e1"
+dependencies = [
+ "glob",
+]
+
+[[package]]
+name = "winapi"
+version = "0.3.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
+dependencies = [
+ "winapi-i686-pc-windows-gnu",
+ "winapi-x86_64-pc-windows-gnu",
+]
+
+[[package]]
+name = "winapi-i686-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
+
+[[package]]
+name = "winapi-util"
+version = "0.1.11"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
+dependencies = [
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "winapi-x86_64-pc-windows-gnu"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
+dependencies = [
+ "windows-collections",
+ "windows-core",
+ "windows-future",
+ "windows-numerics",
+]
+
+[[package]]
+name = "windows-collections"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
+dependencies = [
+ "windows-core",
+]
+
+[[package]]
+name = "windows-core"
+version = "0.62.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
+dependencies = [
+ "windows-implement",
+ "windows-interface",
+ "windows-link",
+ "windows-result",
+ "windows-strings",
+]
+
+[[package]]
+name = "windows-future"
+version = "0.3.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
+dependencies = [
+ "windows-core",
+ "windows-link",
+ "windows-threading",
+]
+
+[[package]]
+name = "windows-implement"
+version = "0.60.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-interface"
+version = "0.59.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "windows-link"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
+
+[[package]]
+name = "windows-numerics"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
+dependencies = [
+ "windows-core",
+ "windows-link",
+]
+
+[[package]]
+name = "windows-result"
+version = "0.4.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-strings"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.45.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
+dependencies = [
+ "windows-targets 0.42.2",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.52.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.59.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
+dependencies = [
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "windows-sys"
+version = "0.61.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
+dependencies = [
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
+dependencies = [
+ "windows_aarch64_gnullvm 0.52.6",
+ "windows_aarch64_msvc 0.52.6",
+ "windows_i686_gnu 0.52.6",
+ "windows_i686_gnullvm",
+ "windows_i686_msvc 0.52.6",
+ "windows_x86_64_gnu 0.52.6",
+ "windows_x86_64_gnullvm 0.52.6",
+ "windows_x86_64_msvc 0.52.6",
+]
+
+[[package]]
+name = "windows-threading"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
+dependencies = [
+ "windows-link",
+]
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
+
+[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
+
+[[package]]
+name = "windows_i686_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
+
+[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.42.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.52.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
+
+[[package]]
+name = "winnow"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "winnow"
+version = "1.0.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
+dependencies = [
+ "memchr",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5"
+dependencies = [
+ "wit-bindgen-rust-macro",
+]
+
+[[package]]
+name = "wit-bindgen"
+version = "0.57.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e"
+
+[[package]]
+name = "wit-bindgen-core"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc"
+dependencies = [
+ "anyhow",
+ "heck",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-bindgen-rust"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21"
+dependencies = [
+ "anyhow",
+ "heck",
+ "indexmap",
+ "prettyplease",
+ "syn",
+ "wasm-metadata",
+ "wit-bindgen-core",
+ "wit-component",
+]
+
+[[package]]
+name = "wit-bindgen-rust-macro"
+version = "0.51.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a"
+dependencies = [
+ "anyhow",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "syn",
+ "wit-bindgen-core",
+ "wit-bindgen-rust",
+]
+
+[[package]]
+name = "wit-component"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2"
+dependencies = [
+ "anyhow",
+ "bitflags",
+ "indexmap",
+ "log",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "wasm-encoder",
+ "wasm-metadata",
+ "wasmparser",
+ "wit-parser",
+]
+
+[[package]]
+name = "wit-parser"
+version = "0.244.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736"
+dependencies = [
+ "anyhow",
+ "id-arena",
+ "indexmap",
+ "log",
+ "semver",
+ "serde",
+ "serde_derive",
+ "serde_json",
+ "unicode-xid",
+ "wasmparser",
+]
+
+[[package]]
+name = "writeable"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
+
+[[package]]
+name = "xattr"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "32e45ad4206f6d2479085147f02bc2ef834ac85886624a23575ae137c8aa8156"
+dependencies = [
+ "libc",
+ "rustix",
+]
+
+[[package]]
+name = "xml-rs"
+version = "0.8.28"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
+
+[[package]]
+name = "xwin"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c337699251ad0c38cf87ee63944de38c2201d017cfbb768e5a3897ae835aacc7"
+dependencies = [
+ "anyhow",
+ "bytes",
+ "cab",
+ "camino",
+ "clap",
+ "cli-table",
+ "crossbeam-channel",
+ "indicatif",
+ "memchr",
+ "mimalloc",
+ "msi",
+ "parking_lot",
+ "rayon",
+ "regex",
+ "serde",
+ "serde_json",
+ "sha2 0.11.0",
+ "tempfile",
+ "toml 1.1.2+spec-1.1.0",
+ "tracing",
+ "tracing-subscriber",
+ "twox-hash",
+ "ureq",
+ "versions",
+ "walkdir",
+ "zip 7.2.0",
+]
+
+[[package]]
+name = "xz2"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "388c44dc09d76f1536602ead6d325eb532f5c122f17782bd57fb47baeeb767e2"
+dependencies = [
+ "lzma-sys",
+]
+
+[[package]]
+name = "yoke"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "abe8c5fda708d9ca3df187cae8bfb9ceda00dd96231bed36e445a1a48e66f9ca"
+dependencies = [
+ "stable_deref_trait",
+ "yoke-derive",
+ "zerofrom",
+]
+
+[[package]]
+name = "yoke-derive"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zerofrom"
+version = "0.1.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
+dependencies = [
+ "zerofrom-derive",
+]
+
+[[package]]
+name = "zerofrom-derive"
+version = "0.1.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+ "synstructure",
+]
+
+[[package]]
+name = "zeroize"
+version = "1.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b97154e67e32c85465826e8bcc1c59429aaaf107c1e4a9e53c8d8ccd5eff88d0"
+
+[[package]]
+name = "zerotrie"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
+dependencies = [
+ "displaydoc",
+ "yoke",
+ "zerofrom",
+]
+
+[[package]]
+name = "zerovec"
+version = "0.11.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
+dependencies = [
+ "yoke",
+ "zerofrom",
+ "zerovec-derive",
+]
+
+[[package]]
+name = "zerovec-derive"
+version = "0.11.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn",
+]
+
+[[package]]
+name = "zip"
+version = "7.2.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c42e33efc22a0650c311c2ef19115ce232583abbe80850bc8b66509ebef02de0"
+dependencies = [
+ "crc32fast",
+ "flate2",
+ "indexmap",
+ "memchr",
+ "typed-path",
+]
+
+[[package]]
+name = "zip"
+version = "8.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d04a6b5381502aa6087c94c669499eb1602eb9c5e8198e534de571f7154809b"
+dependencies = [
+ "bzip2",
+ "crc32fast",
+ "flate2",
+ "indexmap",
+ "lzma-rust2",
+ "memchr",
+ "time",
+ "typed-path",
+ "zopfli",
+ "zstd",
+]
+
+[[package]]
+name = "zlib-rs"
+version = "0.6.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3be3d40e40a133f9c916ee3f9f4fa2d9d63435b5fbe1bfc6d9dae0aa0ada1513"
+
+[[package]]
+name = "zmij"
+version = "1.0.21"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+
+[[package]]
+name = "zopfli"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
+dependencies = [
+ "bumpalo",
+ "crc32fast",
+ "log",
+ "simd-adler32",
+]
+
+[[package]]
+name = "zstd"
+version = "0.13.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e91ee311a569c327171651566e07972200e76fcfe2242a4fa446149a3881c08a"
+dependencies = [
+ "zstd-safe",
+]
+
+[[package]]
+name = "zstd-safe"
+version = "7.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8f49c4d5f0abb602a93fb8736af2a4f4dd9512e36f7f570d66e65ff867ed3b9d"
+dependencies = [
+ "zstd-sys",
+]
+
+[[package]]
+name = "zstd-sys"
+version = "2.0.16+zstd.1.5.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91e19ebc2adc8f83e43039e79776e3fda8ca919132d68a1fed6a5faca2683748"
+dependencies = [
+ "cc",
+ "pkg-config",
+]
diff --git a/engine/Cargo.toml b/engine/Cargo.toml
new file mode 100644
index 0000000..94bfb07
--- /dev/null
+++ b/engine/Cargo.toml
@@ -0,0 +1,23 @@
+[package]
+name = "EyegesturesEngine"
+version = "4.0.0"
+edition = "2021"
+
+[lib]
+crate-type = ["cdylib", "rlib"]
+
+[dependencies]
+# Wasm dependencies
+wasm-bindgen = { version = "0.2", optional = true }
+web-sys = { version = "0.3.99", features = ["console"], optional = true }
+
+# Python dependencies
+# `generate-import-lib` allows Windows builds when maturin uses bundled sysconfig
+# for discovered interpreters.
+pyo3 = { version = "0.28.2", optional = true, features = ["extension-module", "generate-import-lib"] }
+maturin = "1.13.3"
+
+[features]
+default = ["wasm"]
+wasm = ["wasm-bindgen", "web-sys"]
+python = ["pyo3"]
diff --git a/engine/src/calibrator.rs b/engine/src/calibrator.rs
new file mode 100644
index 0000000..c6e7961
--- /dev/null
+++ b/engine/src/calibrator.rs
@@ -0,0 +1,177 @@
+use std::collections::VecDeque;
+
+struct LinearRegression {
+ x_data: Vec>,
+ y_data: Vec,
+ weights: Vec,
+ fitted: bool,
+}
+
+impl LinearRegression {
+ fn new() -> Self {
+ Self { x_data: vec![], y_data: vec![], weights: vec![], fitted: false }
+ }
+
+ fn add_sample(&mut self, x: Vec, y: f64) {
+ // Match JS MLR behavior: include a bias/intercept feature.
+ let mut x_with_bias = x;
+ x_with_bias.push(1.0);
+ self.x_data.push(x_with_bias);
+ self.y_data.push(y);
+ }
+
+ fn fit(&mut self) {
+ let n = self.x_data.len();
+ if n == 0 {
+ self.fitted = false;
+ return;
+ }
+
+ let m = self.x_data[0].len();
+ let mut xtx = vec![vec![0.0f64; m]; m];
+ let mut xty = vec![0.0f64; m];
+
+ for i in 0..n {
+ for j in 0..m {
+ xty[j] += self.x_data[i][j] * self.y_data[i];
+ for k in 0..m {
+ xtx[j][k] += self.x_data[i][j] * self.x_data[i][k];
+ }
+ }
+ }
+
+ for (i, row) in xtx.iter_mut().enumerate() {
+ row[i] += 1e-9;
+ }
+
+ if let Some(w) = gaussian_elimination(xtx, xty) {
+ self.weights = w;
+ self.fitted = true;
+ } else {
+ self.fitted = false;
+ }
+ }
+
+ fn predict(&self, x: &[f64]) -> f64 {
+ if !self.fitted {
+ return 0.0;
+ }
+ let weighted_sum: f64 = x.iter().zip(&self.weights).map(|(a, b)| a * b).sum();
+ let bias = self.weights.get(x.len()).copied().unwrap_or(0.0);
+ weighted_sum + bias
+ }
+}
+
+fn gaussian_elimination(mut a: Vec>, mut b: Vec) -> Option> {
+ let n = a.len();
+ for i in 0..n {
+ let max_row = (i..n).max_by(|&r1, &r2| a[r1][i].abs().partial_cmp(&a[r2][i].abs()).unwrap())?;
+ a.swap(i, max_row);
+ b.swap(i, max_row);
+
+ if a[i][i].abs() < 1e-12 {
+ return None;
+ }
+
+ for k in i + 1..n {
+ let factor = a[k][i] / a[i][i];
+ for j in i..n {
+ a[k][j] -= factor * a[i][j];
+ }
+ b[k] -= factor * b[i];
+ }
+ }
+
+ let mut x = vec![0.0; n];
+ for i in (0..n).rev() {
+ x[i] = b[i];
+ for j in i + 1..n {
+ x[i] -= a[i][j] * x[j];
+ }
+ x[i] /= a[i][i];
+ }
+ Some(x)
+}
+
+const CALIB_POINTS: [[f64; 2]; 25] = [
+ [0.25, 0.25], [0.5, 0.75], [1.0, 0.5], [0.75, 0.5], [0.0, 0.75],
+ [0.5, 0.5], [1.0, 0.25], [0.75, 0.0], [0.25, 0.5], [0.5, 0.0],
+ [0.0, 0.5], [1.0, 1.0], [0.75, 1.0], [0.25, 0.0], [1.0, 0.0],
+ [0.0, 1.0], [0.25, 1.0], [0.75, 0.75],[0.5, 0.25], [0.0, 0.25],
+ [1.0, 0.5], [0.75, 0.25],[0.5, 1.0], [0.25, 0.75], [0.0, 0.0],
+];
+
+pub struct Calibrator {
+ tmp_x: VecDeque>,
+ stable_x: Vec>,
+ tmp_y: VecDeque<[f64; 2]>,
+ stable_y: Vec<[f64; 2]>,
+ reg_x: LinearRegression,
+ reg_y: LinearRegression,
+ point_index: usize,
+ pub fitted: bool,
+}
+
+impl Calibrator {
+ pub fn new() -> Self {
+ Self {
+ tmp_x: VecDeque::new(),
+ stable_x: vec![],
+ tmp_y: VecDeque::new(),
+ stable_y: vec![],
+ reg_x: LinearRegression::new(),
+ reg_y: LinearRegression::new(),
+ point_index: 0,
+ fitted: false,
+ }
+ }
+
+ pub fn add(&mut self, keypoints: Vec, target: [f64; 2]) {
+ self.tmp_x.push_back(keypoints);
+ self.tmp_y.push_back(target);
+
+ if self.tmp_x.len() > 40 {
+ self.tmp_x.pop_front();
+ self.tmp_y.pop_front();
+ }
+
+ let all_x: Vec> = self.tmp_x.iter().chain(self.stable_x.iter()).cloned().collect();
+ let all_y: Vec<[f64; 2]> = self.tmp_y.iter().chain(self.stable_y.iter()).cloned().collect();
+
+ self.reg_x = LinearRegression::new();
+ self.reg_y = LinearRegression::new();
+ for (x, y) in all_x.into_iter().zip(all_y.into_iter()) {
+ self.reg_x.add_sample(x.clone(), y[0]);
+ self.reg_y.add_sample(x, y[1]);
+ }
+
+ self.reg_x.fit();
+ self.reg_y.fit();
+
+ self.fitted = self.reg_x.fitted && self.reg_y.fitted;
+ }
+
+ pub fn predict(&self, keypoints: &[f64]) -> [f64; 2] {
+ if !self.fitted {
+ return [0.0, 0.0];
+ }
+ [self.reg_x.predict(keypoints), self.reg_y.predict(keypoints)]
+ }
+
+ pub fn current_point(&self, screen_width: f64, screen_height: f64) -> [f64; 2] {
+ let p = CALIB_POINTS[self.point_index];
+ [p[0] * screen_width, p[1] * screen_height]
+ }
+
+ pub fn next_point(&mut self) {
+ let all_x: Vec> = self.tmp_x.drain(..).collect();
+ let all_y: Vec<[f64; 2]> = self.tmp_y.drain(..).collect();
+ self.stable_x.extend(all_x);
+ self.stable_y.extend(all_y);
+ self.point_index = (self.point_index + 1) % CALIB_POINTS.len();
+ }
+
+ pub fn reset(&mut self) {
+ *self = Calibrator::new();
+ }
+}
diff --git a/engine/src/eyegestures.rs b/engine/src/eyegestures.rs
new file mode 100644
index 0000000..0044156
--- /dev/null
+++ b/engine/src/eyegestures.rs
@@ -0,0 +1,145 @@
+use crate::{Calibrator, euclidean_distance};
+
+pub struct GazeResult {
+ pub x: f64,
+ pub y: f64,
+ pub calibrating: bool,
+ pub calib_point: [f64; 2],
+}
+
+/// Shared core gaze tracking logic (wasm + python)
+pub struct EyeGesturesCore {
+ pub calibrator: Calibrator,
+ pub screen_width: f64,
+ pub screen_height: f64,
+ pub calib_counter: usize,
+ pub calib_max: usize,
+ pub frames_on_point: usize,
+ pub prev_calib: [f64; 2],
+ pub eye_calib_regions: Vec<[f64; 2]>,
+ pub buffer: Vec<[f64; 2]>,
+ pub start_width: f64,
+ pub start_height: f64,
+ pub head_start: [f64; 2],
+ pub last_keypoints: Vec,
+}
+
+impl EyeGesturesCore {
+ pub fn new(screen_width: f64, screen_height: f64) -> Self {
+ Self {
+ calibrator: Calibrator::new(),
+ screen_width,
+ screen_height,
+ calib_counter: 0,
+ calib_max: 25,
+ frames_on_point: 0,
+ prev_calib: [0.0, 0.0],
+ eye_calib_regions: vec![],
+ buffer: vec![],
+ start_width: 0.0,
+ start_height: 0.0,
+ head_start: [0.0, 0.0],
+ last_keypoints: vec![],
+ }
+ }
+
+ pub fn process_landmarks(&mut self, landmarks: &[[f64; 2]]) -> GazeResult {
+ let left_indices = [33, 133, 160, 159, 158, 157, 173, 155, 154, 153, 144, 145, 153, 246, 468];
+ let right_indices = [362, 263, 387, 386, 385, 384, 398, 382, 381, 380, 374, 373, 374, 466, 473];
+
+ let offset_x = landmarks.iter().map(|l| l[0]).fold(f64::INFINITY, f64::min);
+ let offset_y = landmarks.iter().map(|l| l[1]).fold(f64::INFINITY, f64::min);
+ let max_x = landmarks.iter().map(|l| l[0]).fold(f64::NEG_INFINITY, f64::max);
+ let max_y = landmarks.iter().map(|l| l[1]).fold(f64::NEG_INFINITY, f64::max);
+ let width = max_x - offset_x;
+ let height = max_y - offset_y;
+
+ if self.start_width * self.start_height == 0.0 {
+ self.start_width = width;
+ self.start_height = height;
+ }
+
+ let scale_x = width / self.start_width;
+ let scale_y = height / self.start_height;
+
+ if self.head_start == [0.0, 0.0] {
+ self.head_start = [offset_x * scale_x, offset_y * scale_y];
+ }
+
+ let to_coords = |indices: &[usize]| -> Vec<[f64; 2]> {
+ indices.iter().map(|&i| {
+ let l = landmarks[i];
+ [((l[0] - offset_x) / width) * scale_x,
+ ((l[1] - offset_y) / height) * scale_y]
+ }).collect()
+ };
+
+ let left_eye = to_coords(&left_indices);
+ let right_eye = to_coords(&right_indices);
+
+ let mut keypoints: Vec = left_eye.iter().chain(right_eye.iter())
+ .flat_map(|p| [p[0], p[1]])
+ .collect();
+ keypoints.extend([scale_x, scale_y, width, height]);
+ keypoints.extend([
+ offset_x * scale_x - self.head_start[0],
+ offset_y * scale_y - self.head_start[1],
+ ]);
+
+ self.last_keypoints = keypoints.clone();
+
+ let raw = self.calibrator.predict(&keypoints);
+ self.buffer.push(raw);
+ if self.buffer.len() > 20 {
+ self.buffer.remove(0);
+ }
+ let point = self.buffer.iter().fold([0.0, 0.0], |acc, p| [acc[0] + p[0], acc[1] + p[1]]);
+ let point = [point[0] / self.buffer.len() as f64, point[1] / self.buffer.len() as f64];
+
+ let calibrating = self.calib_counter < self.calib_max;
+ let calib_point = self.calibrator.current_point(self.screen_width, self.screen_height);
+
+ if calibrating {
+ self.eye_calib_regions.push(left_eye[0]);
+ self.calibrator.add(keypoints, calib_point);
+
+ let dist = euclidean_distance(&point, &calib_point);
+ if dist < 0.1 * self.screen_width && self.frames_on_point > 20 {
+ self.calibrator.next_point();
+ self.frames_on_point = 0;
+ self.buffer.clear();
+ } else if dist < 0.1 * self.screen_width {
+ self.frames_on_point += 1;
+ }
+
+ if self.prev_calib != calib_point {
+ self.prev_calib = calib_point;
+ self.calib_counter += 1;
+ }
+ } else {
+ let eye_calib_radius = 0.01;
+ let seen = self.eye_calib_regions.iter()
+ .any(|r| euclidean_distance(r, &left_eye[0]) < eye_calib_radius);
+
+ if !seen {
+ self.calib_max += 5;
+ }
+ }
+
+ let x = point[0].clamp(0.0, self.screen_width);
+ let y = point[1].clamp(0.0, self.screen_height);
+
+ GazeResult { x, y, calibrating, calib_point }
+ }
+
+ pub fn add_calibration_point(&mut self, x: f64, y: f64) {
+ self.calib_counter += 1;
+ self.calib_max += 1;
+ self.calibrator.add(self.last_keypoints.clone(), [x, y]);
+ }
+
+ pub fn recalibrate(&mut self) {
+ self.calibrator.reset();
+ self.calib_counter = 0;
+ }
+}
diff --git a/engine/src/fixation.rs b/engine/src/fixation.rs
new file mode 100644
index 0000000..dafdd93
--- /dev/null
+++ b/engine/src/fixation.rs
@@ -0,0 +1,25 @@
+pub struct Fixation {
+ center: [f64; 2],
+ radius: f64,
+ pub level: f64,
+}
+
+impl Fixation {
+ pub fn new(x: f64, y: f64, radius: f64) -> Self {
+ Self { center: [x, y], radius, level: 0.0 }
+ }
+
+ pub fn process(&mut self, x: f64, y: f64) -> f64 {
+ let dx = x - self.center[0];
+ let dy = y - self.center[1];
+
+ if dx * dx + dy * dy < self.radius * self.radius {
+ self.level = (self.level + 0.02).min(1.0);
+ } else {
+ self.center = [x, y];
+ self.level = 0.0;
+ }
+
+ self.level
+ }
+}
diff --git a/engine/src/lib.rs b/engine/src/lib.rs
new file mode 100644
index 0000000..5c88a86
--- /dev/null
+++ b/engine/src/lib.rs
@@ -0,0 +1,32 @@
+mod calibrator;
+mod fixation;
+mod eyegestures;
+
+#[cfg(target_arch = "wasm32")]
+pub mod wasm;
+
+#[cfg(feature = "python")]
+pub mod python;
+
+#[cfg(feature = "python")]
+use pyo3::prelude::*;
+
+pub use calibrator::Calibrator;
+pub use fixation::Fixation;
+pub use eyegestures::EyeGesturesCore;
+
+#[cfg(feature = "python")]
+#[pymodule]
+#[pyo3(name = "EyegesturesEngine")]
+fn eyegestures_engine(_py: Python<'_>, module: &Bound<'_, PyModule>) -> PyResult<()> {
+ module.add_class::()?;
+ Ok(())
+}
+
+pub fn euclidean_distance(a: &[f64], b: &[f64]) -> f64 {
+ a.iter()
+ .zip(b.iter())
+ .map(|(x, y)| (x - y).powi(2))
+ .sum::()
+ .sqrt()
+}
diff --git a/engine/src/python.rs b/engine/src/python.rs
new file mode 100644
index 0000000..6676904
--- /dev/null
+++ b/engine/src/python.rs
@@ -0,0 +1,56 @@
+use crate::eyegestures::EyeGesturesCore;
+use pyo3::prelude::*;
+
+#[pyclass]
+pub struct EyeGesturesEnginePython{
+ core: EyeGesturesCore,
+}
+
+#[pymethods]
+impl EyeGesturesEnginePython {
+ #[new]
+ pub fn new(screen_width: f64, screen_height: f64) -> Self {
+ Self{
+ core: EyeGesturesCore::new(screen_width, screen_height),
+ }
+ }
+
+ /// Process landmarks and return [x, y, calibrating, calib_x, calib_y]
+ pub fn process(&mut self, landmarks: Vec) -> Vec {
+ let landmarks: Vec<[f64; 2]> = landmarks
+ .chunks_exact(2)
+ .map(|pair| [pair[0], pair[1]])
+ .collect();
+
+ let result = self.core.process_landmarks(&landmarks);
+ vec![
+ result.x,
+ result.y,
+ if result.calibrating { 1.0 } else { 0.0 },
+ result.calib_point[0],
+ result.calib_point[1],
+ ]
+ }
+
+ /// Manually add a calibration point at (x, y)
+ pub fn add_calibration_point(&mut self, x: f64, y: f64) {
+ self.core.add_calibration_point(x, y);
+ }
+
+ /// Reset calibration state
+ pub fn recalibrate(&mut self) {
+ self.core.recalibrate();
+ }
+
+ /// Get current calibration counter
+ #[getter]
+ pub fn calib_counter(&self) -> usize {
+ self.core.calib_counter
+ }
+
+ /// Get current calibration max
+ #[getter]
+ pub fn calib_max(&self) -> usize {
+ self.core.calib_max
+ }
+}
diff --git a/engine/src/wasm.rs b/engine/src/wasm.rs
new file mode 100644
index 0000000..78d76bc
--- /dev/null
+++ b/engine/src/wasm.rs
@@ -0,0 +1,41 @@
+use crate::eyegestures::EyeGesturesCore;
+use wasm_bindgen::prelude::*;
+
+#[wasm_bindgen]
+pub struct EyeGesturesEngineWasm{
+ core: EyeGesturesCore,
+}
+
+#[wasm_bindgen]
+impl EyeGesturesEngineWasm {
+ #[wasm_bindgen(constructor)]
+ pub fn new(screen_width: f64, screen_height: f64) -> Self {
+ Self{
+ core: EyeGesturesCore::new(screen_width, screen_height),
+ }
+ }
+
+ pub fn process(&mut self, landmarks: Vec) -> Vec {
+ let landmarks: Vec<[f64; 2]> = landmarks
+ .chunks_exact(2)
+ .map(|pair| [pair[0], pair[1]])
+ .collect();
+
+ let result = self.core.process_landmarks(&landmarks);
+ vec![
+ result.x,
+ result.y,
+ if result.calibrating { 1.0 } else { 0.0 },
+ result.calib_point[0],
+ result.calib_point[1],
+ ]
+ }
+
+ pub fn add_calibration_point(&mut self, x: f64, y: f64) {
+ self.core.add_calibration_point(x, y);
+ }
+
+ pub fn recalibrate(&mut self) {
+ self.core.recalibrate();
+ }
+}
diff --git a/examples/simple_example_v4.py b/examples/simple_example_v4.py
new file mode 100644
index 0000000..d7db1b0
--- /dev/null
+++ b/examples/simple_example_v4.py
@@ -0,0 +1,81 @@
+import os
+import sys
+import cv2
+import pygame
+import numpy as np
+
+pygame.init()
+pygame.font.init()
+
+# Get the display dimensions
+screen_info = pygame.display.Info()
+screen_width = screen_info.current_w
+screen_height = screen_info.current_h
+
+# Set up the screen
+screen = pygame.display.set_mode((screen_width, screen_height))
+pygame.display.set_caption("EyeGestures v4 example")
+font_size = 48
+bold_font = pygame.font.Font(None, font_size)
+bold_font.set_bold(True) # Set the font to bold
+
+dir_path = os.path.dirname(os.path.realpath(__file__))
+sys.path.append(f'{dir_path}/..')
+
+from eyeGestures.utils import VideoCapture
+from eyeGestures import EyeGestures_v4
+
+gestures = EyeGestures_v4()
+cap = VideoCapture(0)
+
+# Initialize Pygame
+# Set up colors
+RED = (255, 0, 100)
+BLUE = (100, 0, 255)
+GREEN = (0, 255, 0)
+BLANK = (0,0,0)
+WHITE = (255, 255, 255)
+
+clock = pygame.time.Clock()
+
+# Main game loop
+running = True
+iterator = 0
+prev_x = 0
+prev_y = 0
+while running:
+ # Event handling
+ for event in pygame.event.get():
+ if event.type == pygame.QUIT:
+ running = False
+ elif event.type == pygame.KEYDOWN:
+ if event.key == pygame.K_q and pygame.key.get_mods() & pygame.KMOD_CTRL:
+ running = False
+
+
+ # Generate new random position for the cursor
+ ret, frame = cap.read()
+ frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
+
+ point, calibrate, calib_point = gestures.step(frame, screen_width, screen_height - 100)
+
+ screen.fill((0, 0, 0))
+ # Display frame on Pygame
+ if calibrate:
+ if calib_point[0] != prev_x or calib_point[1] != prev_y:
+ iterator += 1
+ prev_x = calib_point[0]
+ prev_y = calib_point[1]
+ # pygame.draw.circle(screen, GREEN, fit_point, calibration_radius)
+ pygame.draw.circle(screen, BLUE, calib_point, 50)
+ else:
+ pass
+
+ pygame.draw.circle(screen, RED, point, 50)
+ pygame.display.flip()
+
+ # Cap the frame rate
+ clock.tick(60)
+
+# Quit Pygame
+pygame.quit()
diff --git a/eyeGestures/EyegesturesEngine.cpython-313-x86_64-linux-gnu.so b/eyeGestures/EyegesturesEngine.cpython-313-x86_64-linux-gnu.so
new file mode 100755
index 0000000..a54fd13
Binary files /dev/null and b/eyeGestures/EyegesturesEngine.cpython-313-x86_64-linux-gnu.so differ
diff --git a/eyeGestures/__init__.py b/eyeGestures/__init__.py
index 599731a..d550605 100644
--- a/eyeGestures/__init__.py
+++ b/eyeGestures/__init__.py
@@ -1,64 +1,47 @@
-import pickle
-import time
from typing import Any, Dict, List, Optional, Tuple
import cv2
import numpy as np
import numpy.typing as npt
-import eyeGestures.screenTracker.dataPoints as dp
-from eyeGestures.calibration_v1 import Calibrator as Calibrator_v1
-from eyeGestures.calibration_v2 import Calibrator as Calibrator_v2
from eyeGestures.face import Face, FaceFinder
from eyeGestures.Fixation import Fixation
-from eyeGestures.gazeEstimator import GazeTracker
-from eyeGestures.gevent import Cevent, Gevent
-from eyeGestures.utils import low_pass_filter_fourier, recoverable
+from eyeGestures.utils import recoverable
+
+try:
+ from .EyegesturesEngine import EyeGesturesEnginePython as RustEyeGesturesEngine # type: ignore[import-untyped]
+except ImportError:
+ try:
+ from EyegesturesEngine import (
+ EyeGesturesEnginePython as RustEyeGesturesEngine, # type: ignore[import-not-found, import-untyped]
+ )
+ except ImportError:
+ RustEyeGesturesEngine = None
-VERSION = "3.0.0"
+VERSION = "4.0.0"
-class EyeGestures_v3:
+class EyeGestures_v4:
"""Main class for EyeGesture tracker. It configures and manages entire algorithm"""
def __init__(self, calibration_radius: int = 1000) -> None:
self.calibration_radius = calibration_radius
- self.clb: Dict[str, Calibrator_v2] = dict() # Calibrator_v2()
- self.cap = None
- self.calibration: Dict[str, bool] = dict()
- self.average_points: Dict[str, npt.NDArray[np.float64]] = dict()
- self.filled_points: Dict[str, int] = dict()
- self.enable_CN = False
- self.calibrate_gestures = False
+ self.average_points: npt.NDArray[np.float64] = np.zeros((1, 2))
+ self.calibration: bool = False
+ self.filled_points: int = 0
self.finder = FaceFinder()
self.face = Face()
+ self.engine: Optional[Any] = None
- # this has to be contexted
- self.prev_timestamp: Dict[str, float] = dict()
- self.prev_point: Dict[str, npt.NDArray[np.float64]] = dict()
self.fix: Optional[float] = None
- self.velocity_max: Dict[str, int] = dict()
- self.velocity_min: Dict[str, int] = dict()
self.fixationTracker: Dict[str, Fixation] = dict()
self.key_points_buffer: Dict[str, List[npt.NDArray[np.float64]]] = dict()
self.starting_head_position = np.zeros((1, 2))
self.starting_size = np.zeros((1, 2))
- def saveModel(self, context: str = "main") -> Optional[bytes]:
- if context in self.clb:
- return pickle.dumps(self.clb[context])
- return None
-
- def loadModel(self, model: Any, context: str = "main") -> None:
- self.clb[context] = pickle.loads(model)
-
- def uploadCalibrationMap(self, points: npt.NDArray[np.float64], context: str = "main") -> None:
- self.addContext(context)
- self.clb[context].updMatrix(np.array(points))
-
def getLandmarks(self, frame: cv2.typing.MatLike) -> Tuple[npt.NDArray[np.float64], bool, cv2.typing.MatLike]:
frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
@@ -66,7 +49,6 @@ def getLandmarks(self, frame: cv2.typing.MatLike) -> Tuple[npt.NDArray[np.float6
# try:
self.face.process(frame, self.finder.find(frame))
-
face_landmarks = self.face.get_landmarks()
assert face_landmarks is not None
l_eye = self.face.get_left_eye()
@@ -119,353 +101,38 @@ def getLandmarks(self, frame: cv2.typing.MatLike) -> Tuple[npt.NDArray[np.float6
]
return key_points, blink, subframe
- def whichAlgorithm(self, context: str = "main") -> str:
- if context in self.clb:
- return self.clb[context].whichAlgorithm()
- return "None"
+ def whichAlgorithm(self) -> str:
+ return "rust engine"
- def reset(self, context: str = "main") -> None:
- self.filled_points[context] = 0
- if context in self.clb:
- self.addContext(context)
+ def reset(self) -> None:
+ self.filled_points = 0
def setFixation(self, fix: float) -> None:
self.fix = fix
- def addContext(self, context: str) -> None:
- if context not in self.clb:
- self.clb[context] = Calibrator_v2(self.calibration_radius)
- self.average_points[context] = np.zeros((20, 2))
- self.filled_points[context] = 0
- self.calibration[context] = False
- self.prev_timestamp[context] = time.time()
- self.prev_point[context] = np.array((0.0, 0.0))
- self.velocity_max[context] = 0
- self.velocity_min[context] = 100000000
- self.fixationTracker[context] = Fixation(0, 0, 100)
- self.key_points_buffer[context] = []
-
- @recoverable(ret_error_params=(None, None))
- def step(
- self, frame: cv2.typing.MatLike, calibration: bool, width: int, height: int, context: str = "main"
- ) -> Tuple[Optional[Gevent], Optional[Cevent]]:
- self.addContext(context)
-
- self.calibration[context] = calibration
-
- key_points, blink, sub_frame = self.getLandmarks(frame)
-
- self.key_points_buffer[context].append(key_points)
- if len(self.key_points_buffer[context]) > 10:
- self.key_points_buffer[context].pop(0)
- key_points = low_pass_filter_fourier(key_points, 200)
-
- y_point = self.clb[context].predict(key_points)
- self.average_points[context][1:, :] = self.average_points[context][
- : (self.average_points[context].shape[0] - 1), :
- ]
- self.average_points[context][0, :] = y_point
-
- if (
- self.filled_points[context] < self.average_points[context].shape[0]
- and (y_point != np.array([0.0, 0.0])).any()
- ):
- self.filled_points[context] += 1
- if self.filled_points[context] == 0:
- self.filled_points[context] = 1
-
- averaged_point = np.sum(self.average_points[context][:, :], axis=0) / (self.filled_points[context])
- fixation = self.fixationTracker[context].process(averaged_point[0], averaged_point[1])
-
- duration = time.time() - self.prev_timestamp[context]
- velocity = abs(averaged_point - self.prev_point[context]) / duration
- velocity = np.sqrt(velocity[0] ** 2 + velocity[1] ** 2)
- self.prev_point[context] = averaged_point
- self.prev_timestamp[context] = time.time()
-
- self.velocity_max[context] = max(self.velocity_max[context], velocity)
- self.velocity_min[context] = min(self.velocity_min[context], velocity)
-
- saccades = velocity > (self.velocity_max[context]) / 4
-
- if self.calibration[context] and (
- self.clb[context].insideClbRadius(averaged_point, width, height)
- or self.filled_points[context] < self.average_points[context].shape[0] * 10
- ):
- self.clb[context].add(key_points, self.clb[context].getCurrentPoint(width, height))
- else:
- self.clb[context].post_fit()
-
- if self.calibration[context] and self.clb[context].insideAcptcRadius(averaged_point, width, height):
- if self.clb[context].isReadyToMove():
- self.clb[context].movePoint()
-
- gevent = Gevent(
- point=averaged_point,
- blink=blink,
- fixation=fixation,
- saccades=saccades,
- sub_frame=sub_frame,
- )
- cevent = Cevent(
- self.clb[context].getCurrentPoint(width, height),
- self.clb[context].acceptance_radius,
- self.clb[context].calibration_radius,
- )
- return (gevent, cevent)
-
-
-class EyeGestures_v2:
- """Main class for EyeGesture tracker. It configures and manages entire algorithm"""
-
- def __init__(self, calibration_radius: int = 1000) -> None:
- self.monitor_width = 1
- self.monitor_height = 1
- self.calibration_radius = calibration_radius
-
- self.clb: Dict[str, Calibrator_v2] = dict() # Calibrator_v2()
- self.cap = None
- self.gestures = EyeGestures_v1(285, 115, 200, 100)
-
- self.calibration: Dict[str, bool] = dict()
-
- self.CN: int = 5
-
- self.average_points: Dict[str, npt.NDArray[np.float64]] = dict()
- self.filled_points: Dict[str, int] = dict()
- self.enable_CN = False
- self.calibrate_gestures = False
-
- self.fix = 0.8
-
- def saveModel(self, context: str = "main") -> Optional[bytes]:
- if context in self.clb:
- return pickle.dumps(self.clb[context])
- return None
-
- def loadModel(self, model: Any, context: str = "main") -> None:
- self.clb[context] = pickle.loads(model)
-
- def uploadCalibrationMap(self, points: npt.NDArray[np.float64], context="main") -> None:
- self.addContext(context)
- self.clb[context].updMatrix(np.array(points))
-
- def getLandmarks(
- self, frame: cv2.typing.MatLike, calibrate: bool = False, context: str = "main"
- ) -> Tuple[npt.NDArray[np.float64], npt.NDArray[np.float64], bool, bool, Optional[Cevent]]:
-
- frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
- frame = cv2.flip(frame, 1)
- # frame = cv2.resize(frame, (360, 640))
-
- event, cevent = self.gestures.step(
- frame,
- context,
- calibrate, # set calibration - switch to False to stop calibration
- self.monitor_width,
- self.monitor_height,
- 0,
- 0,
- self.fix,
- 100,
- )
-
- if event is not None or cevent is not None:
- cursor_x, cursor_y = event.point[0], event.point[1]
- l_eye_landmarks = event.l_eye.getLandmarks()
- r_eye_landmarks = event.r_eye.getLandmarks()
-
- cursors = np.array([cursor_x, cursor_y]).reshape(1, 2)
- eye_events = np.array([event.blink, event.fixation]).reshape(1, 2)
- key_points = np.concatenate((cursors, l_eye_landmarks, r_eye_landmarks, eye_events))
- return (
- np.array((cursor_x, cursor_y)),
- key_points,
- event.blink,
- event.fixation,
- cevent,
- )
- return np.array((0.0, 0.0)), np.array([]), False, False, None
-
- def whichAlgorithm(self, context: str = "main") -> str:
- if context in self.clb:
- return self.clb[context].whichAlgorithm()
- return "None"
-
- def setClassicImpact(self, impact: int) -> None:
- self.CN = impact
-
- def reset(self, context: str = "main") -> None:
- self.filled_points[context] = 0
- if context in self.clb:
- self.addContext(context)
-
- def setFixation(self, fix: float) -> None:
- self.fix = fix
-
- def setClassicalImpact(self, CN: int) -> None:
- self.CN = CN
-
- def enableCNCalib(self) -> None:
- self.enable_CN = True
-
- def disableCNCalib(self) -> None:
- self.enable_CN = False
-
- def addContext(self, context: str) -> None:
- if context not in self.clb:
- self.clb[context] = Calibrator_v2(self.calibration_radius)
- self.average_points[context] = np.zeros((20, 2))
- self.filled_points[context] = 0
- self.calibration[context] = False
-
- @recoverable(ret_error_params=(None, None))
- def step(
- self, frame: cv2.typing.MatLike, calibration: bool, width: int, height: int, context: str = "main"
- ) -> Tuple[Optional[Gevent], Optional[Cevent]]:
- self.addContext(context)
-
- self.calibration[context] = calibration
- self.monitor_width = width
- self.monitor_height = height
-
- classic_point, key_points, blink, fixation, cevent = self.getLandmarks(
- frame, self.calibrate_gestures and self.enable_CN, context=context
- )
-
- margin = 10
- if cevent is None:
- raise ValueError("cevent must not be None")
- if (classic_point[0] <= margin) and self.calibration[context]:
- self.calibrate_gestures = cevent.calibration
- elif (classic_point[0] >= width - margin) and self.calibration[context]:
- self.calibrate_gestures = cevent.calibration
- elif (cevent.point[1] <= margin) and self.calibration[context]:
- self.calibrate_gestures = cevent.calibration
- elif (classic_point[1] >= height - margin) and self.calibration[context]:
- self.calibrate_gestures = cevent.calibration
- else:
- self.calibrate_gestures = False
-
- y_point = self.clb[context].predict(key_points)
- self.average_points[context][1:, :] = self.average_points[context][
- : (self.average_points[context].shape[0] - 1), :
- ]
- if fixation <= self.fix:
- self.average_points[context][0, :] = y_point
-
- if (
- self.filled_points[context] < self.average_points[context].shape[0]
- and (y_point != np.array([0.0, 0.0])).any()
- ):
- self.filled_points[context] += 1
- averaged_point = (np.sum(self.average_points[context][:, :], axis=0) + (classic_point * self.CN)) / (
- self.filled_points[context] + self.CN
- )
-
- if self.calibration[context] and (
- self.clb[context].insideClbRadius(averaged_point, width, height)
- or self.filled_points[context] < self.average_points[context].shape[0] * 10
- ):
- self.clb[context].add(key_points, self.clb[context].getCurrentPoint(width, height))
- else:
- self.clb[context].post_fit()
-
- if self.calibration[context] and self.clb[context].insideAcptcRadius(averaged_point, width, height):
- if self.clb[context].isReadyToMove():
- self.clb[context].movePoint()
-
- gevent = Gevent(averaged_point, blink, fixation)
- cevent = Cevent(
- self.clb[context].getCurrentPoint(width, height),
- self.clb[context].acceptance_radius,
- self.clb[context].calibration_radius,
- )
- return (gevent, cevent)
-
-
-class EyeGestures_v1:
- """Main class for EyeGesture tracker. It configures and manages entier algorithm"""
-
- def __init__(self, roi_x=225, roi_y=105, roi_width=80, roi_height=15):
-
- screen_width = 500
- screen_height = 500
- height = 250
- width = 250
-
- roi_x = roi_x % screen_width
- roi_y = roi_y % screen_height
- roi_width = roi_width % screen_width
- roi_height = roi_height % screen_height
-
- self.screen_width = screen_width
- self.screen_height = screen_height
-
- self.screen = dp.Screen(screen_width, screen_height)
-
- self.gaze = GazeTracker(
- screen_width,
- screen_height,
- width,
- height,
- roi_x,
- roi_y,
- roi_width,
- roi_height,
- )
-
- self.calibrators = dict()
- self.calibrate = False
-
- def getFeatures(self, image):
- """[NOT RECOMMENDED] Function allowing for extraction of gaze features from image"""
-
- return self.gaze.getFeatures(image)
-
- # @timeit
- # 0.011 - 0.015 s for execution
+ @recoverable(ret_error_params=(None, None, None))
def step(
self,
- image,
- context,
- calibration,
- display_width,
- display_height,
- display_offset_x=0,
- display_offset_y=0,
- fixation_freeze=0.7,
- freeze_radius=20,
- offset_x=0,
- offset_y=0,
+ frame: cv2.typing.MatLike,
+ width: int,
+ height: int,
):
- """Function performing estimation and returning event object"""
- display = dp.Display(display_width, display_height, display_offset_x, display_offset_y)
-
- # allow for random recalibration shot
- event = self.gaze.estimate(
- image,
- display,
- context,
- calibration,
- fixation_freeze,
- freeze_radius,
- offset_x,
- offset_y,
- )
- cevent = None
+ if self.engine is None:
+ self.engine = RustEyeGesturesEngine(width, height)
- if event is not None:
- cursor_x, cursor_y = event.point[0], event.point[1]
- if context in self.calibrators:
- self.calibrate = self.calibrators[context].calibrate(cursor_x, cursor_y, event.fixation)
- else:
- self.calibrators[context] = Calibrator_v1(display_width, display_height, cursor_x, cursor_y)
- self.calibrate = self.calibrators[context].calibrate(cursor_x, cursor_y, event.fixation)
+ find_result = self.finder.find(frame)
+ if find_result is None or not find_result.face_landmarks:
+ raise ValueError("No face landmarks detected")
- cpoint = self.calibrators[context].get_current_point()
- calibration = self.calibrate and cpoint != (0, 0)
- cevent = Cevent(cpoint, 100, 100, calibration)
+ landmarks = find_result.face_landmarks[0]
+ scaled_landmarks = np.array([(landmark.x, landmark.y) for landmark in landmarks], dtype=np.float64)
+ assert self.engine is not None
+ result = self.engine.process(scaled_landmarks.flatten().tolist())
- return (event, cevent)
+ x, y = result[0], result[1]
+ is_calibrating = result[2] == 1.0
+ calib_x, calib_y = result[3], result[4]
+ calib_x = min(max(calib_x, 0), width)
+ calib_y = min(max(calib_y, 0), height)
+ return [x, y], is_calibrating, [calib_x, calib_y]
diff --git a/eyeGestures/calibration_v1.py b/eyeGestures/calibration_v1.py
deleted file mode 100644
index 12d037a..0000000
--- a/eyeGestures/calibration_v1.py
+++ /dev/null
@@ -1,165 +0,0 @@
-import time
-
-
-class CalibrationPositions:
- LEFT = "left"
- RIGHT = "right"
- TOP = "top"
- BOTTOM = "bottom"
-
-
-class Calibrator:
-
- def __init__(self, width, height, start_x, start_y):
- self.width = width
- self.height = height
- self.start_x = start_x
- self.start_y = start_y
-
- self.prev_x = 0
- self.prev_y = 0
-
- self.calibration_margin = 200
- self.calibration_steps = []
- self.__set_order()
-
- self.calibrate_left = False
- self.calibrate_right = False
- self.calibrate_top = False
- self.calibrate_bottom = False
-
- self.calibration = False
- self.drawn = False
- self.prev_point = None
- self.last_calib = time.time()
-
- def __add_left(self):
- self.calibration_steps.append(CalibrationPositions.LEFT)
- return self
-
- def __add_right(self):
- self.calibration_steps.append(CalibrationPositions.RIGHT)
- return self
-
- def __add_top(self):
- self.calibration_steps.append(CalibrationPositions.TOP)
- return self
-
- def __add_bottom(self):
- self.calibration_steps.append(CalibrationPositions.BOTTOM)
- return self
-
- def __set_order(self):
- if self.start_x < self.width / 2 and CalibrationPositions.LEFT not in self.calibration_steps:
- self.__add_left().__add_right()
- elif self.start_x > self.width / 2 and CalibrationPositions.RIGHT not in self.calibration_steps:
- self.__add_right().__add_left()
-
- if self.start_y < self.height / 2 and CalibrationPositions.TOP not in self.calibration_steps:
- self.__add_top().__add_bottom()
- elif self.start_y > self.height / 2 and CalibrationPositions.BOTTOM not in self.calibration_steps:
- self.__add_bottom().__add_top()
-
- def add_recalibrate(self, recalibrate_step):
-
- if recalibrate_step not in self.calibration_steps:
- self.calibration_steps.append(recalibrate_step)
-
- def get_current_point(self):
- if len(self.calibration_steps) > 0:
- if CalibrationPositions.LEFT == self.calibration_steps[0]:
- return (self.calibration_margin, int(self.height / 2))
- if CalibrationPositions.RIGHT == self.calibration_steps[0]:
- return (self.width - self.calibration_margin, int(self.height / 2))
- if CalibrationPositions.TOP == self.calibration_steps[0]:
- return (int(self.width / 2), self.calibration_margin)
- if CalibrationPositions.BOTTOM == self.calibration_steps[0]:
- return (int(self.width / 2), self.height - self.calibration_margin)
- return (0, 0)
-
- def calibrate(self, x, y, fix):
-
- if abs(x - self.width / 2) > 150 and self.prev_point in [
- CalibrationPositions.TOP,
- CalibrationPositions.BOTTOM,
- ]:
- if x - self.width / 2 < 0:
- self.add_recalibrate(CalibrationPositions.LEFT)
- else:
- self.add_recalibrate(CalibrationPositions.RIGHT)
-
- if abs(y - self.height / 2) > 150 and self.prev_point in [
- CalibrationPositions.LEFT,
- CalibrationPositions.RIGHT,
- ]:
- if y - self.height / 2 < 0:
- self.add_recalibrate(CalibrationPositions.TOP)
- else:
- self.add_recalibrate(CalibrationPositions.BOTTOM)
-
- self.prev_y = y
- self.prev_x = x
- if len(self.calibration_steps) <= 0:
- self.prev_point = None
- return False
-
- fixation_thresh = 0.3
- if fix > fixation_thresh and (time.time() - self.last_calib) > 5.0:
- if CalibrationPositions.LEFT == self.calibration_steps[0] and x < self.calibration_margin:
- if CalibrationPositions.LEFT in self.calibration_steps:
- self.calibration_steps.remove(CalibrationPositions.LEFT)
- self.prev_point = CalibrationPositions.LEFT
- self.drawn = False
- self.last_calib = time.time()
- return True
- if CalibrationPositions.RIGHT == self.calibration_steps[0] and x > self.width - self.calibration_margin:
- if CalibrationPositions.RIGHT in self.calibration_steps:
- self.calibration_steps.remove(CalibrationPositions.RIGHT)
- self.prev_point = CalibrationPositions.RIGHT
- self.drawn = False
- self.last_calib = time.time()
- return True
- if CalibrationPositions.TOP == self.calibration_steps[0] and y < self.calibration_margin:
- if CalibrationPositions.TOP in self.calibration_steps:
- self.calibration_steps.remove(CalibrationPositions.TOP)
- self.prev_point = CalibrationPositions.TOP
- self.drawn = False
- self.last_calib = time.time()
- return True
- if CalibrationPositions.BOTTOM == self.calibration_steps[0] and y > self.height - self.calibration_margin:
- if CalibrationPositions.BOTTOM in self.calibration_steps:
- self.calibration_steps.remove(CalibrationPositions.BOTTOM)
- self.prev_point = CalibrationPositions.BOTTOM
- self.drawn = False
- self.last_calib = time.time()
- return True
- # TODO: somewhere here is bug breaking entire program
- self.last_calib = time.time()
- self.drawn = False
- self.prev_point = None
-
- if self.calibration_steps[0] in [
- CalibrationPositions.RIGHT,
- CalibrationPositions.LEFT,
- ]:
- if x < self.width / 2:
- if CalibrationPositions.RIGHT in self.calibration_steps:
- self.calibration_steps.remove(CalibrationPositions.RIGHT)
- self.calibration_steps.insert(0, CalibrationPositions.RIGHT)
- else:
- if CalibrationPositions.LEFT in self.calibration_steps:
- self.calibration_steps.remove(CalibrationPositions.LEFT)
- self.calibration_steps.insert(0, CalibrationPositions.LEFT)
- return True
-
- if self.calibration_steps[0] is CalibrationPositions.TOP:
- self.calibration_steps.insert(0, CalibrationPositions.BOTTOM)
- return True
- self.calibration_steps.insert(0, CalibrationPositions.TOP)
- return True
-
- self.prev_point = None
- return False
-
- def calibrated(self):
- return len(self.calibration_steps) <= 0
diff --git a/eyeGestures/calibration_v2.py b/eyeGestures/calibration_v2.py
deleted file mode 100644
index e5ec973..0000000
--- a/eyeGestures/calibration_v2.py
+++ /dev/null
@@ -1,201 +0,0 @@
-import threading
-from typing import List
-
-import numpy as np
-import numpy.typing as npt
-import sklearn.linear_model as scireg
-
-
-def euclidean_distance(point1: npt.NDArray[np.float64], point2: npt.NDArray[np.float64]) -> np.float64:
- return np.linalg.norm(point1 - point2)
-
-
-class Calibrator:
-
- PRECISION_LIMIT = 50
- PRECISION_STEP = 10
- ACCEPTANCE_RADIUS = 500
- CALIBRATION_RADIUS = 1000
-
- def __init__(self, calibration_radius: int = 1000) -> None:
- self.X: List[npt.NDArray[np.float64]] = []
- self.Y_y: List[npt.NDArray[np.float64]] = []
- self.Y_x: List[npt.NDArray[np.float64]] = []
- self.__tmp_X: List[npt.NDArray[np.float64]] = []
- self.__tmp_Y_y: List[npt.NDArray[np.float64]] = []
- self.__tmp_Y_x: List[npt.NDArray[np.float64]] = []
- self.reg = None
- self.reg_x = scireg.Ridge(alpha=0.5)
- self.reg_y = scireg.Ridge(alpha=0.5)
- self.current_algorithm = "Ridge"
- self.fitted = False
- self.cv_not_set = True
- self.fixations_x = None
- self.fixations_y = None
-
- self.matrix = CalibrationMatrix()
-
- self.precision_limit = self.PRECISION_LIMIT
- self.precision_step = self.PRECISION_STEP
- self.acceptance_radius = int(calibration_radius / 2)
- self.calibration_radius = int(calibration_radius)
-
- self.lock = threading.Lock()
- self.calcualtion_coroutine = threading.Thread(target=self.__async_post_fit)
- self.fit_coroutines: List[threading.Thread] = []
-
- def __launch_fit(self) -> None:
- coroutine = threading.Thread(target=self.__async_fit)
- self.fit_coroutines.append(coroutine)
- coroutine.start()
- self.__join_finished()
-
- def __join_finished(self) -> None:
- for coroutine in self.fit_coroutines:
- if not coroutine.is_alive():
- coroutine.join()
-
- def add(self, x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> None:
- with self.lock:
- self.__tmp_X.append(x.flatten())
- self.__tmp_Y_y.append(y[1])
- self.__tmp_Y_x.append(y[0])
- self.__launch_fit()
-
- # This coroutine helps to asynchronously recalculate results
- def __async_fit(self) -> None:
- try:
- with self.lock:
- __fit_tmp_X = np.array(self.__tmp_X + self.X, dtype=object)
- __fit_tmp_Y_y = np.array(self.__tmp_Y_y + self.Y_y)
- __fit_tmp_Y_x = np.array(self.__tmp_Y_x + self.Y_x)
- self.reg_x.fit(__fit_tmp_X, __fit_tmp_Y_x)
- self.reg_y.fit(__fit_tmp_X, __fit_tmp_Y_y)
- self.fitted = True
- except Exception as e:
- print(f"Exception as {e}")
-
- # This coroutine helps to asynchronously recalculate results
- def __async_post_fit(self) -> None:
- try:
- tmp_fixations_x = scireg.LassoCV(cv=50, max_iter=10000)
- tmp_fixations_y = scireg.LassoCV(cv=50, max_iter=10000)
-
- __tmp_X = np.array(self.X)
- __tmp_Y_y = np.array(self.Y_y)
- __tmp_Y_x = np.array(self.Y_x)
-
- tmp_fixations_x.fit(__tmp_X, __tmp_Y_x)
- tmp_fixations_y.fit(__tmp_X, __tmp_Y_y)
-
- with self.lock:
- self.fixations_x = tmp_fixations_x
- self.fixations_y = tmp_fixations_y
- self.fitted = True
- self.current_algorithm = "LassoCV"
- except Exception as e:
- print(f"Exception as {e}")
- self.cv_not_set = True
-
- def post_fit(self) -> None:
- if self.cv_not_set:
- # self.calcualtion_coroutine.start()
- self.cv_not_set = False
-
- def whichAlgorithm(self) -> str:
- with self.lock:
- return self.current_algorithm
-
- def predict(self, x: npt.NDArray[np.float64]) -> npt.NDArray[np.float64]:
- with self.lock:
- if self.fitted:
- x = x.flatten()
- x = x.reshape(1, -1)
- y_x = self.reg_x.predict(x)[0]
- y_y = self.reg_y.predict(x)[0]
- return np.array([y_x, y_y])
- return np.array([0.0, 0.0])
-
- def movePoint(self) -> None:
- with self.lock:
- self.X = self.X + self.__tmp_X
- self.Y_y = self.Y_y + self.__tmp_Y_y
- self.Y_x = self.Y_x + self.__tmp_Y_x
- self.matrix.movePoint()
- self.__tmp_X = []
- self.__tmp_Y_y = []
- self.__tmp_Y_x = []
-
- def isReadyToMove(self) -> int:
- return len(self.__tmp_X) > 30 # magic number - collect 30 points
-
- def getCurrentPoint(self, width: int, heigth: int) -> npt.NDArray[np.float64]:
- return self.matrix.getCurrentPoint(width, heigth)
-
- def updMatrix(self, points: npt.NDArray[np.float64]) -> None:
- self.matrix.updMatrix(points)
-
- def unfit(self) -> None:
- self.acceptance_radius = self.ACCEPTANCE_RADIUS
- self.calibration_radius = self.CALIBRATION_RADIUS
- self.fitted = False
-
- def increase_precision(self) -> None:
- if self.acceptance_radius > self.precision_limit:
- self.acceptance_radius -= self.precision_step
- if self.calibration_radius > self.precision_limit and self.acceptance_radius < self.calibration_radius:
- self.calibration_radius -= self.precision_step
-
- def insideClbRadius(self, point: npt.NDArray[np.float64], width: int, height: int) -> np.bool_:
- return euclidean_distance(point, self.getCurrentPoint(width, height)) < self.calibration_radius
-
- def insideAcptcRadius(self, point: npt.NDArray[np.float64], width: int, height: int) -> np.bool_:
- return euclidean_distance(point, self.getCurrentPoint(width, height)) < self.acceptance_radius
-
-
-class CalibrationMatrix:
-
- def __init__(self) -> None:
-
- self.iterator = 0
- self.points: npt.NDArray[np.float64] = np.array(
- [
- [1, 0.5],
- [0.75, 0.5],
- [0.5, 0.5],
- [0.25, 0.5],
- [0.0, 0.5],
- [1.0, 1.0],
- [0.75, 1.0],
- [0.5, 1.0],
- [0.25, 1.0],
- [0.0, 1.0],
- [1.0, 0.0],
- [0.75, 0.0],
- [0.5, 0.0],
- [0.25, 0.0],
- [0.0, 0.0],
- [0.0, 0.75],
- [0.75, 0.75],
- [0.5, 0.75],
- [0.25, 0.75],
- [0.0, 0.75],
- [1.0, 0.25],
- [0.75, 0.25],
- [0.5, 0.25],
- [0.25, 0.25],
- [0.0, 0.25],
- ]
- )
-
- def updMatrix(self, points: npt.NDArray[np.float64]) -> None:
- self.points = points
- self.iterator = 0
-
- def movePoint(self) -> None:
- self.iterator += 1
- self.iterator %= len(self.points)
-
- def getCurrentPoint(self, width: int = 1, height: int = 1) -> npt.NDArray[np.float64]:
- it = self.iterator
- return np.array([self.points[it, 0] * width, self.points[it, 1] * height])
diff --git a/eyeGestures/eye.py b/eyeGestures/eye.py
index 184c4ee..490f838 100644
--- a/eyeGestures/eye.py
+++ b/eyeGestures/eye.py
@@ -3,9 +3,9 @@
from typing import Optional, Tuple
import cv2
-import mediapipe as mp
import numpy as np
import numpy.typing as npt
+from mediapipe.tasks.python.vision.face_landmarker import FaceLandmarksConnections
from eyeGestures.utils import Buffor
@@ -13,8 +13,24 @@
class Eye:
"""Class storing data related and representing a eye"""
- LEFT_EYE_KEYPOINTS = np.array(list(mp.solutions.face_mesh.FACEMESH_LEFT_EYE))[:, 0]
- RIGHT_EYE_KEYPOINTS = np.array(list(mp.solutions.face_mesh.FACEMESH_RIGHT_EYE))[:, 0]
+ LEFT_EYE_KEYPOINTS = np.array(
+ sorted(
+ {
+ index
+ for connection in FaceLandmarksConnections.FACE_LANDMARKS_LEFT_EYE
+ for index in (connection.start, connection.end)
+ }
+ )
+ )
+ RIGHT_EYE_KEYPOINTS = np.array(
+ sorted(
+ {
+ index
+ for connection in FaceLandmarksConnections.FACE_LANDMARKS_RIGHT_EYE
+ for index in (connection.start, connection.end)
+ }
+ )
+ )
LEFT_EYE_PUPIL_KEYPOINT = [473]
RIGHT_EYE_PUPIL_KEYPOINT = [468]
@@ -37,8 +53,8 @@ def __init__(self, side: int) -> None:
self.y = 0
self.width = 0
self.height = 0
- self.center_x = 0
- self.center_y = 0
+ self.center_x: float = 0.0
+ self.center_y: float = 0.0
self.image: Optional[cv2.typing.MatLike] = None
self.pupil: Optional[npt.NDArray[np.float64]] = None
self.offset: Optional[npt.NDArray[np.float64]] = None
@@ -146,19 +162,19 @@ def _process(self, image: cv2.typing.MatLike, region: npt.NDArray[np.float64]) -
masked_image = cv2.bitwise_not(background, cv2.cvtColor(image.copy(), cv2.COLOR_BGR2GRAY), mask=mask)
margin = 2
- min_x = np.min(region_int[:, 0]) - margin
- max_x = np.max(region_int[:, 0]) + margin
- min_y = np.min(region_int[:, 1]) - margin
- max_y = np.max(region_int[:, 1]) + margin
+ min_x = int(np.min(region_int[:, 0]) - margin)
+ max_x = int(np.max(region_int[:, 0]) + margin)
+ min_y = int(np.min(region_int[:, 1]) - margin)
+ max_y = int(np.max(region_int[:, 1]) + margin)
self.x = min_x
self.y = min_y
- self.width = np.max(region_int[:, 0]) - np.min(region_int[:, 0])
- self.height = np.max(region_int[:, 1]) - np.min(region_int[:, 1])
+ self.width = int(np.max(region_int[:, 0]) - np.min(region_int[:, 0]))
+ self.height = int(np.max(region_int[:, 1]) - np.min(region_int[:, 1]))
- self.center_x = (min_x + max_x) / 2
- self.center_y = (min_y + max_y) / 2
+ self.center_x = float((min_x + max_x) / 2)
+ self.center_y = float((min_y + max_y) / 2)
# HACKETY_HACK:
assert self.pupil is not None
diff --git a/eyeGestures/face.py b/eyeGestures/face.py
index 42575eb..ef481ec 100644
--- a/eyeGestures/face.py
+++ b/eyeGestures/face.py
@@ -1,37 +1,70 @@
"""Module providing finding and extraction of face from image."""
+import os
+import urllib.request
+from pathlib import Path
from typing import Any, NamedTuple, Optional, Tuple
import cv2
import mediapipe as mp
import numpy as np
import numpy.typing as npt
+from mediapipe.tasks import python
+from mediapipe.tasks.python import vision
from eyeGestures.eye import Eye
+FACE_LANDMARKER_MODEL_URL = (
+ "https://storage.googleapis.com/mediapipe-models/face_landmarker/face_landmarker/float16/1/face_landmarker.task"
+)
+
+
+def _resolve_model_asset_path(model_asset_path: Optional[str]) -> str:
+ """Resolve the FaceLandmarker model path."""
+
+ if model_asset_path:
+ return model_asset_path
+
+ env_model_path = os.getenv("EYEGESTURES_FACE_LANDMARKER_MODEL")
+ if env_model_path:
+ return env_model_path
+
+ default_model_path = Path(__file__).with_name("face_landmarker_v2_with_blendshapes.task")
+ if default_model_path.exists():
+ return str(default_model_path)
+
+ default_model_path.parent.mkdir(parents=True, exist_ok=True)
+ urllib.request.urlretrieve(FACE_LANDMARKER_MODEL_URL, default_model_path)
+ return str(default_model_path)
+
class FaceFinder:
"""Class helping finding face"""
- def __init__(self) -> None:
- self.mp_face_mesh = mp.solutions.face_mesh.FaceMesh(
- refine_landmarks=True,
- static_image_mode=False,
- min_detection_confidence=0.5,
- min_tracking_confidence=0.5,
+ def __init__(self, model_asset_path: Optional[str] = None) -> None:
+ resolved_model_path = _resolve_model_asset_path(model_asset_path)
+ base_options = python.BaseOptions(model_asset_path=resolved_model_path)
+ options = vision.FaceLandmarkerOptions(
+ base_options=base_options,
+ output_face_blendshapes=True,
+ output_facial_transformation_matrixes=True,
+ num_faces=1,
)
+ self.face_landmarker = vision.FaceLandmarker.create_from_options(options)
def find(self, image: cv2.typing.MatLike) -> Optional[Any]:
- """Find face mesh"""
+ """Find face landmarks."""
assert len(image.shape) > 2
try:
- face_mesh = self.mp_face_mesh.process(cv2.cvtColor(image, cv2.COLOR_BGR2RGB))
- if face_mesh.multi_face_landmarks is None:
+ rgb_image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)
+ mp_image = mp.Image(image_format=mp.ImageFormat.SRGB, data=rgb_image)
+ detection_result = self.face_landmarker.detect(mp_image)
+ if not detection_result.face_landmarks:
return None
- return face_mesh
+ return detection_result
except Exception as e:
print(f"Exception in FaceFinder: {e}")
return None
@@ -77,15 +110,13 @@ def get_landmarks(self) -> Optional[npt.NDArray[np.float64]]:
return self.landmarks
def _landmarks(self, face: Any) -> npt.NDArray[np.float64]:
+ face_landmarks = face.face_landmarks[0]
- __complex_landmark_points = face.multi_face_landmarks
- __complex_landmarks = __complex_landmark_points[0].landmark
-
- __face_landmarks = []
- for landmark in __complex_landmarks:
- __face_landmarks.append((landmark.x * self.image_w, landmark.y * self.image_h))
+ scaled_landmarks = []
+ for landmark in face_landmarks:
+ scaled_landmarks.append((landmark.x * self.image_w, landmark.y * self.image_h))
- return np.array(__face_landmarks)
+ return np.array(scaled_landmarks)
def process(self, image: cv2.typing.MatLike, face: Optional[Any]) -> None:
"""Process face landmarks on image"""
diff --git a/eyeGestures/gazeEstimator.py b/eyeGestures/gazeEstimator.py
deleted file mode 100644
index fb31df9..0000000
--- a/eyeGestures/gazeEstimator.py
+++ /dev/null
@@ -1,273 +0,0 @@
-import numpy as np
-
-import eyeGestures.screenTracker.dataPoints as dp
-from eyeGestures.face import Face, FaceFinder
-from eyeGestures.Fixation import Fixation
-from eyeGestures.gaze_contexter import GazeContext
-from eyeGestures.gevent import Gevent
-from eyeGestures.processing import EyeProcessor
-from eyeGestures.screenTracker.screenTracker import ScreenManager
-from eyeGestures.utils import Buffor
-
-
-def isInside(circle_x, circle_y, r, x, y):
- """Function checking if points are inside the circle"""
-
- # Compare radius of circle
- # with distance of its center
- # from given point
- if (x - circle_x) * (x - circle_x) + (y - circle_y) * (y - circle_y) <= r * r:
- return True
- return False
-
-
-class GazeTracker:
- """Class processing images and returning gaze tracker"""
-
- N_FEATURES = 16
-
- def __init__(
- self,
- screen_width,
- screen_heigth,
- eye_screen_w,
- eye_screen_h,
- roi_x,
- roi_y,
- roi_width,
- roi_height,
- ):
- self.screen = dp.Screen(screen_width, screen_heigth)
-
- self.offset_x = 0
- self.offset_y = 0
-
- self.roi_x = roi_x
- self.roi_y = roi_y
- self.roi_width = roi_width
- self.roi_height = roi_height
-
- self.eye_screen_w = eye_screen_w
- self.eye_screen_h = eye_screen_h
-
- self.eyeProcessorLeft = EyeProcessor(eye_screen_w, eye_screen_h)
- self.eyeProcessorRight = EyeProcessor(eye_screen_w, eye_screen_h)
-
- self.screen_man = ScreenManager()
-
- self.finder = FaceFinder()
- self.face = Face()
-
- # those are used for analysis
-
- self.point_screen = [0.0, 0.0]
- self.freezed_point = [0.0, 0.0]
-
- self.GContext = GazeContext()
-
- # self.calibration = False
-
- def __gaze_intersection(self, l_eye, r_eye, l_buff, r_buff):
- l_pupil = l_eye.getPupil()
- l_gaze = l_eye.getGaze(l_buff)
-
- r_pupil = r_eye.getPupil()
- r_gaze = r_eye.getGaze(r_buff)
-
- l_end = l_gaze + l_pupil
- r_end = r_gaze + r_pupil
-
- l_m = (l_end[1] - l_pupil[1]) / (l_end[0] - l_pupil[0])
- r_m = (r_end[1] - r_pupil[1]) / (r_end[0] - r_pupil[0])
-
- l_b = l_end[1] - l_m * l_end[0]
- r_b = r_end[1] - r_m * r_end[0]
-
- i_x = (r_b - l_b) / (l_m - r_m)
- i_y = r_m * i_x + r_b
- return (i_x, i_y)
-
- def __pupil(self, eye, eyeProcessor, intersection_x, buffor):
- eyeProcessor.append(eye.getPupil(), eye.getLandmarks(), buffor)
- point = eyeProcessor.getAvgPupil(self.eye_screen_w, self.eye_screen_h, buffor)
- point = np.array((int(intersection_x), point[1]))
-
- return point, buffor
-
- def estimate(
- self,
- image,
- display,
- context_id,
- calibration,
- fixation_freeze=0.7,
- freeze_radius=20,
- offset_x=0,
- offset_y=0,
- ):
- """Function estimating gaze and returning gaze event based on image"""
-
- event = None
- face_mesh = self.getFeatures(image)
- if not face_mesh:
- return None
-
- if face_mesh.multi_face_landmarks:
- self.face.process(image, face_mesh)
-
- context = self.GContext.get(
- context_id,
- display,
- face=None,
- roi=dp.ScreenROI(self.roi_x, self.roi_y, self.roi_width, self.roi_height),
- edges=dp.ScreenROI(285, 105, 80, 15),
- cluster_boundaries=dp.ScreenROI(225, 125, 20, 20),
- buffor=Buffor(200),
- l_pupil=Buffor(20),
- r_pupil=Buffor(20),
- l_eye_buff=Buffor(20),
- r_eye_buff=Buffor(20),
- fixation=Fixation(0, 0, 100),
- )
- context.calibration = calibration
-
- if self.face is not None:
- if self.face.landmarks is None:
- return event
-
- if context.face is None:
- x, y, w, h = self.face.get_bounding_box()
- i_w = self.face.image_w
- i_h = self.face.image_h
- context.face = (x, y, w, h, i_w, i_h)
-
- l_eye = self.face.get_left_eye()
- r_eye = self.face.get_right_eye()
-
- # TODO: check what happens here before with l_pupil
- intersection_x, _ = self.__gaze_intersection(l_eye, r_eye, context.l_eye_buff, context.r_eye_buff)
- l_point, l_buffor = self.__pupil(l_eye, self.eyeProcessorLeft, intersection_x, context.l_pupil)
- r_point, r_buffor = self.__pupil(r_eye, self.eyeProcessorRight, intersection_x, context.r_pupil)
-
- context.l_pupil = l_buffor
- context.r_pupil = r_buffor
-
- compound_point = np.array(((l_point + r_point) / 2), dtype=np.uint32)
-
- blink = l_eye.getBlink() or r_eye.getBlink()
- if blink != True:
- context.gaze_buffor.add(compound_point)
- # current face radius
- _, _, face_w, face_h = self.face.get_bounding_box()
- image_w = self.face.image_w
- image_h = self.face.image_h
-
- face_w_perc = face_w / image_w
- face_h_perc = face_h / image_h
-
- # # prev current face radius
- _, _, c_face_w, c_face_h, c_image_w, c_image_h = context.face
-
- c_face_w_perc = c_face_w / c_image_w
- c_face_h_perc = c_face_h / c_image_h
-
- x, y, w, h = self.face.get_bounding_box()
- i_w = self.face.image_w
- i_h = self.face.image_h
- context.face = (x, y, w, h, i_w, i_h)
-
- # roi update
-
- # context.roi.x -= diff_face_x * 500 # current size of virtual display
- # context.roi.y -= diff_face_y * 500 # current size of virtual display
- if abs(face_w_perc / c_face_w_perc - 1.0) > 0.02:
- context.roi.width = context.roi.width * abs(face_w_perc / c_face_w_perc)
- # context.edges.width= context.edges.width * abs(face_w_perc/c_face_w_perc)
- context.gaze_buffor.flush()
- # context.calibration = True
- if abs(face_h_perc / c_face_h_perc - 1.0) > 0.02:
- context.roi.height = context.roi.height * abs(face_h_perc / c_face_h_perc)
- # context.edges.height= context.edges.height * abs(face_w_perc/c_face_w_perc)
- context.gaze_buffor.flush()
- # context.calibration = True
-
- self.point_screen, roi, cluster = self.screen_man.process(
- context.gaze_buffor,
- context.roi,
- context.edges,
- self.screen,
- context.display,
- context.calibration,
- (offset_x, offset_y),
- )
-
- context.roi = roi
- if cluster:
- x, y, width, height = cluster.getBoundaries()
- context.cluster_boundaries.x = x
- context.cluster_boundaries.y = y
- context.cluster_boundaries.width = width
- context.cluster_boundaries.height = height
-
- self.GContext.update(context_id, context)
-
- ###########################################################
-
- fix = context.fixation.process(self.point_screen[0], self.point_screen[1])
- # this should prevent of sudden movement down when blinking - not perfect yet
-
- if fix > fixation_freeze:
- r = freeze_radius
- if not isInside(
- self.freezed_point[0],
- self.freezed_point[1],
- r,
- self.point_screen[0],
- self.point_screen[1],
- ):
- self.freezed_point = self.point_screen
-
- event = Gevent(
- self.freezed_point,
- blink,
- fix,
- l_eye,
- r_eye,
- display,
- context.roi,
- context.edges,
- context.cluster_boundaries,
- context_id,
- )
- else:
- self.freezed_point = self.point_screen
- event = Gevent(
- self.point_screen,
- blink,
- fix,
- l_eye,
- r_eye,
- display,
- context.roi,
- context.edges,
- context.cluster_boundaries,
- context_id,
- )
-
- return event
-
- # def get_contextes(self):
- # """Function returning contextes"""
-
- # return self.GContext.get_contextes()
-
- # def add_offset(self,x,y):
- # """Function adding offsets to tracker"""
-
- # self.screen_man.push_window(x,y)
-
- def getFeatures(self, image):
- """Function returning face landmarks"""
-
- face_mesh = self.finder.find(image)
- return face_mesh
diff --git a/eyeGestures/gaze_contexter.py b/eyeGestures/gaze_contexter.py
deleted file mode 100644
index 312c78c..0000000
--- a/eyeGestures/gaze_contexter.py
+++ /dev/null
@@ -1,124 +0,0 @@
-"""Module providing a core tracking features."""
-
-import eyeGestures.screenTracker.dataPoints as dp
-from eyeGestures.Fixation import Fixation
-from eyeGestures.utils import Buffor
-
-
-class Contexter:
- """Class storing contextes"""
-
- def __init__(self):
- self.context = {}
-
- def add_context(self, context_id, context):
- """Function allowing to add new context to contexter"""
-
- if context_id not in self.context:
- self.context[context_id] = context
- return True
- return False
-
- def remove_context(self, context_id):
- """Function removing context"""
- return self.context.pop(context_id, None) is not None
-
- def get_context(self, context_id):
- """Function returning context based on id"""
- return self.context.get(context_id, None)
-
- def update_context(self, context_id, data):
- """Function updating context with new data"""
-
- if context_id in self.context:
- self.context[context_id] = data
- else:
- self.add_context(context_id, data)
- return True
-
- def get_number_contexts(self):
- """Function returning number of contexts"""
-
- return len(self.context)
-
-
-class Gcontext:
- """Helper class for Gcontext"""
-
- def __init__(
- self,
- display,
- face,
- roi,
- edges,
- cluster_boundaries,
- gaze_buffor,
- l_pupil,
- r_pupil,
- l_eye_buff,
- r_eye_buff,
- fixation,
- calibration,
- ):
-
- self.roi = roi
- self.face = face
- self.edges = edges
- self.cluster_boundaries = cluster_boundaries
- self.gaze_buffor = gaze_buffor
- self.l_pupil = l_pupil
- self.r_pupil = r_pupil
- self.l_eye_buff = l_eye_buff
- self.r_eye_buff = r_eye_buff
- self.display = display
- self.fixation = fixation
- self.calibration = calibration
-
-
-class GazeContext:
- """Context wrapper for gaze tracker application"""
-
- def __init__(self):
- self.contexter = Contexter()
-
- def get(
- self,
- context_id,
- display,
- face=None,
- roi=dp.ScreenROI(285, 105, 80, 15),
- edges=dp.ScreenROI(285, 105, 80, 15),
- cluster_boundaries=dp.ScreenROI(225, 125, 20, 20),
- buffor=Buffor(200),
- l_pupil=Buffor(20),
- r_pupil=Buffor(20),
- l_eye_buff=Buffor(20),
- r_eye_buff=Buffor(20),
- fixation=Fixation(0, 0, 100),
- calibration=False,
- ):
- """Function creating new context or returning if id already exists"""
-
- context = Gcontext(
- display=display,
- face=face,
- roi=roi,
- edges=edges,
- cluster_boundaries=cluster_boundaries,
- gaze_buffor=buffor,
- l_pupil=l_pupil,
- r_pupil=r_pupil,
- l_eye_buff=l_eye_buff,
- r_eye_buff=r_eye_buff,
- fixation=fixation,
- calibration=calibration,
- )
-
- if self.contexter.add_context(context_id, context):
- return context
- return self.contexter.get_context(context_id)
-
- def update(self, context_id, context):
- """Function updating existing context"""
-
- self.contexter.update_context(context_id, context)
diff --git a/eyeGestures/processing.py b/eyeGestures/processing.py
index 9a5f8e5..4c1bc69 100644
--- a/eyeGestures/processing.py
+++ b/eyeGestures/processing.py
@@ -50,7 +50,7 @@ def append(self, pupil: Tuple[int, int], landmarks: npt.NDArray[np.float64], pup
)
def __convertPoint(self, point, width=1.0, height=1.0, scale_w=1.0, scale_h=1.0, offset=(0.0, 0.0)):
- (min_x, min_y) = offset
+ min_x, min_y = offset
x = int(((point[0] - min_x) / scale_w) * width)
y = int(((point[1] - min_y) / scale_h) * height)
return (x, y)
diff --git a/eyeGestures/scalling_test.py b/eyeGestures/scalling_test.py
deleted file mode 100644
index b39cdf3..0000000
--- a/eyeGestures/scalling_test.py
+++ /dev/null
@@ -1,60 +0,0 @@
-import eyeGestures.screenTracker.dataPoints as dp
-import eyeGestures.screenTracker.screenTracker as scrtr
-
-# region of interest inside the virtual screen
-ROI_WIDTH = 30
-ROI_HEIGHT = 20
-
-####################### screen 2 display Tests #################################
-
-
-def test_scaleUpTest():
- pos = (50, 50)
- BIGGER = 10
-
- roi = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH, ROI_HEIGHT)
- roi2 = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH + BIGGER, ROI_HEIGHT + BIGGER)
-
- bigger_roi = scrtr.scaleUp(roi, roi2, 0.1)
-
- assert bigger_roi.width > roi.width
- assert bigger_roi.height > roi.height
-
-
-def test_scaleUpTest_known():
- pos = (50, 50)
- BIGGER = 10
-
- roi = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH, ROI_HEIGHT)
- roi2 = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH + BIGGER, ROI_HEIGHT + BIGGER)
-
- bigger_roi = scrtr.scaleUp(roi, roi2, 0.1)
-
- assert int(bigger_roi.width) == int(ROI_WIDTH * 1.1)
- assert int(bigger_roi.height) == int(ROI_HEIGHT * 1.1)
-
-
-def test_scaleDownTest():
- pos = (50, 50)
- SMALLER = 10
-
- roi = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH, ROI_HEIGHT)
- roi2 = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH - SMALLER, ROI_HEIGHT - SMALLER)
-
- smaller_roi = scrtr.scaleDown(roi, roi2, -0.1)
-
- assert smaller_roi.width < roi.width
- assert smaller_roi.height < roi.height
-
-
-def test_scaleDownTest_known():
- pos = (50, 50)
- SMALLER = 10
-
- roi = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH, ROI_HEIGHT)
- roi2 = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH - SMALLER, ROI_HEIGHT - SMALLER)
-
- smaller_roi = scrtr.scaleDown(roi, roi2, -0.1)
-
- assert int(smaller_roi.width) == int(ROI_WIDTH * 0.9)
- assert int(smaller_roi.height) == int(ROI_HEIGHT * 0.9)
diff --git a/eyeGestures/screenTracker/__init__.py b/eyeGestures/screenTracker/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/eyeGestures/screenTracker/clusters.py b/eyeGestures/screenTracker/clusters.py
deleted file mode 100644
index 38d4d53..0000000
--- a/eyeGestures/screenTracker/clusters.py
+++ /dev/null
@@ -1,91 +0,0 @@
-import numpy as np
-from sklearn.cluster import DBSCAN
-
-
-class Cluster:
- """Class representing one ROI cluster"""
-
- def __init__(self, label, points):
-
- self.label = label
- self.points = np.array(points)
- self.weight = len(self.points)
- self.__centroid = self.centroid(points)
-
- x, y, w, h = self.boundaries(points)
-
- self.x = x
- self.y = y
- self.w = w
- self.h = h
-
- def centroid(self, points):
- """Function calculating and returning center of ROI"""
-
- x, y, w, h = self.boundaries(points)
-
- c_x, c_y = (x + w / 2, y + h / 2)
-
- x, y = sum(points) / len(points)
-
- c_x, c_y = (c_x + x) / 2, (c_y + y) / 2
- return (c_x, c_y)
-
- def boundaries(self, points):
- """Function calculating and returning boundaries of cluster"""
-
- x = np.min(points[:, 0])
- width = abs(np.max(points[:, 0]) - x)
-
- y = np.min(points[:, 1])
- height = abs(np.max(points[:, 1]) - y)
-
- return (x, y, width, height)
-
- def getBoundaries(self):
- """Function returning boundaries of cluster"""
- return (self.x, self.y, self.w, self.h)
-
- def getCenter(self):
- """Function returning center of ROI"""
- return self.__centroid
-
-
-class Clusters:
- """Algorithm dividing tracked points into clusters so it can be estimated were is center of ROI"""
-
- def __init__(self, buffer):
- buffer = np.array(buffer)
-
- self.head = len(buffer) - 1
- self.main_cluster = None
- self.clusters = []
-
- dbScan = DBSCAN(eps=12, min_samples=3)
- clustering = dbScan.fit(buffer)
-
- labels = clustering.labels_
- unique_labels = set(labels) - {-1}
-
- for u_label in unique_labels:
- core_sample_indices = clustering.core_sample_indices_
- cluster_points = buffer[core_sample_indices][labels[clustering.core_sample_indices_] == u_label]
- self.clusters.append(Cluster(u_label, cluster_points))
-
- # return sorted(self.clusters, key=lambda cluster: cluster.weight)[-1]
- if len(self.clusters) > 0:
- self.main_cluster = self.clusters[labels[self.head]]
- else:
- self.main_cluster = None
-
- def clearPoints(self):
- """[Possibly dead code] Function reseting cluster"""
- self.head = 0
-
- def getClusters(self):
- """Function returning all clusters"""
- return self.clusters
-
- def getMainCluster(self):
- """Function returning main clusters"""
- return self.main_cluster
diff --git a/eyeGestures/screenTracker/dataPoints.py b/eyeGestures/screenTracker/dataPoints.py
deleted file mode 100644
index fe1c296..0000000
--- a/eyeGestures/screenTracker/dataPoints.py
+++ /dev/null
@@ -1,61 +0,0 @@
-class Center:
- """Helper representing Center of object with x,y,width,height"""
-
- def __init__(self, x, y, width, height):
- self.x = (x + width) / 2
- self.y = (y + height) / 2
-
-
-class Screen:
- """Helper representing screen by making box of width,height"""
-
- def __init__(self, width, height):
- self.x = 0
- self.y = 0
- self.width = width
- self.height = height
-
- def getCenter(self):
- """Function returnig center of the screen"""
- return Center(self.x, self.y, self.width, self.height)
-
-
-class ScreenROI:
- """Helper representing ROI by making box of width,height with x and y offset"""
-
- def __init__(self, x, y, width, height):
- self.x = x
- self.y = y
- self.width = width
- self.height = height
-
- def setCenter(self, x, y):
- """Function allowing to move ROI by changing its center"""
- self.x = x - self.width / 2
- self.y = y - self.height / 2
-
- def getCenter(self):
- """Function returnig center of the ROI"""
- return Center(self.x, self.y, self.width, self.height)
-
- def getBoundaries(self):
- """Function returnig boundaries of the ROI"""
- return (self.x, self.y, self.width, self.height)
-
-
-class Display:
- """Helper representing Display by making box of width,height with x and y offset"""
-
- def __init__(self, width, height, offset_x, offset_y):
- self.width = width
- self.height = height
- self.offset_x = offset_x
- self.offset_y = offset_y
-
- # self.buffor = Buffor(20)
-
- def getCenter(self):
- """Function returnig center of the Display"""
- center_x = (self.offset_x + self.width) / 2
- center_y = (self.offset_y + self.height) / 2
- return (center_x, center_y)
diff --git a/eyeGestures/screenTracker/heatmap.py b/eyeGestures/screenTracker/heatmap.py
deleted file mode 100644
index 7a6a87a..0000000
--- a/eyeGestures/screenTracker/heatmap.py
+++ /dev/null
@@ -1,68 +0,0 @@
-import numpy as np
-
-
-class Heatmap:
- """Helper representing Heatmap of tracked points"""
-
- def __init__(self, width, height, buffor):
- self.inc_step = 10
- self.step = 10
- bars = self.step
-
- self.width = width
- self.height = height
-
- bars_x = int(width / self.step)
- bars_y = int(height / self.step)
-
- self.axis_x = np.zeros((bars_x))
- self.axis_y = np.zeros((bars_y))
-
- for point in buffor:
- x = point[0]
- y = point[1]
-
- self.axis_x[min(abs(int(x / bars)), bars_x - 1)] += self.inc_step
- self.axis_y[min(abs(int(y / bars)), bars_x - 1)] += self.inc_step
-
- self.min_x = self.__getParam((self.axis_x > self.inc_step * 4), last=False)
- self.max_x = self.__getParam((self.axis_x > self.inc_step * 4), last=True)
- self.min_y = self.__getParam((self.axis_y > self.inc_step * 4), last=False)
- self.max_y = self.__getParam((self.axis_y > self.inc_step * 4), last=True)
-
- def __getParam(self, param, last: bool = False):
- ret = 0
- retArray = np.where(param)
-
- if len(retArray[0]) > 0:
- ret = retArray[0][-int(last)] * self.step
-
- if not ret == np.nan:
- ret = int(ret)
-
- return ret
-
- def getBoundaries(self):
- """Function returning boundaries of heatmap"""
- x = self.min_x
- y = self.min_y
- w = self.max_x - self.min_x
- h = self.max_y - self.min_y
- return (x, y, w, h)
-
- def getCenter(self):
- """Function returning center of heatmap"""
- center_x = int((self.max_x - self.min_x) / 2 + self.min_x)
- center_y = int((self.max_y - self.min_y) / 2 + self.min_y)
- return (center_x, center_y)
-
- def getPeak(self):
- """Function returning peak of heatmap"""
- x = int(np.argmax(self.axis_x) * self.inc_step)
- y = int(np.argmax(self.axis_y) * self.inc_step)
- return (x, y)
-
- def getHist(self):
- """Function returning histogram of the heatmap.
- This heatmap is simply 2d histogram, so this function returns values for each axis."""
- return (self.axis_x / self.inc_step, self.axis_y / self.inc_step)
diff --git a/eyeGestures/screenTracker/screenTracker.py b/eyeGestures/screenTracker/screenTracker.py
deleted file mode 100644
index 7441b3f..0000000
--- a/eyeGestures/screenTracker/screenTracker.py
+++ /dev/null
@@ -1,221 +0,0 @@
-import eyeGestures.screenTracker.dataPoints as dp
-from eyeGestures.screenTracker.clusters import Clusters
-from eyeGestures.screenTracker.heatmap import Heatmap
-
-# THIS FILE IS SLOWLY BECOMING BLACK MAGIC
-
-
-def detect_if_inside(point, rect):
- px = point[0]
- py = point[1]
- x, y, width, height = rect.getBoundaries()
-
- x_in = x < px < x + width
- y_in = y < py < y + height
-
- return x_in and y_in
-
-
-def detect_edges(roi, display, point_on_screen, point_on_display):
- """Function performing edge detection based on point, screen and display sizes"""
- (s_x, s_y) = point_on_screen
- (d_x, d_y) = point_on_display
-
- x, y, width, height = roi.getBoundaries()
- new_roi = dp.ScreenROI(x, y, width, height)
-
- # new_w = screen.width
- # margin = 5
- # TODO: fix this to estimate w and h
- if d_x <= 0:
- new_roi.width = new_roi.width + abs(new_roi.x - s_x)
- new_roi.x = s_x
-
- if d_x >= display.width:
- new_roi.width = abs(new_roi.x - s_x)
-
- if d_y <= 0:
- new_roi.height = new_roi.height + abs(new_roi.y - s_y)
- new_roi.y = s_y
-
- if d_y >= display.height:
- new_roi.height = abs(new_roi.y - s_y)
-
- return new_roi
-
-
-def rescale_h(roi, scale_h, change=0.5):
- """change size on height"""
- scale_diff_h = abs(1.0 - scale_h)
-
- ret_heigth = roi.height
-
- if scale_diff_h > change:
- ret_heigth = roi.height / 1.0 * scale_h
-
- return ret_heigth
-
-
-def rescale_w(roi, scale_w, change=0.5):
- """change size on width"""
- scale_diff_w = abs(1.0 - scale_w)
-
- ret_width = roi.width
-
- if scale_diff_w > change:
- ret_width = roi.width / 1.0 * scale_w
-
- return ret_width
-
-
-def scaleDown(roi, edge, scale):
- """Function scalling down one roi till edges are met"""
-
- (_, _, cluster_w, cluster_h) = edge.getBoundaries()
-
- new_roi = dp.ScreenROI(roi.x, roi.y, roi.width, roi.height)
- if cluster_w < roi.width:
- new_scale_w = 1.0 + scale
- new_w = rescale_w(roi, new_scale_w, scale)
- new_roi.width = new_w
-
- if cluster_h < roi.height:
- new_scale_h = 1.0 + scale
- new_h = rescale_h(roi, new_scale_h, scale)
- new_roi.height = new_h
-
- return new_roi
-
-
-def scaleUp(roi, roi2, scale):
- """Function scalling up one roi into another roi,
- second roi is for limiting scaling operation and scale is telling how much you can scale"""
-
- (_, _, roi2_w, roi2_h) = roi2.getBoundaries()
-
- new_roi = dp.ScreenROI(roi.x, roi.y, roi.width, roi.height)
-
- if roi2_w > roi.width:
- new_scale_w = 1.0 + scale
- new_w = rescale_w(roi, new_scale_w, scale)
- new_roi.width = new_w
-
- if roi2_h > roi.height:
- new_scale_h = 1.0 + scale
- new_h = rescale_h(roi, new_scale_h, scale)
- new_roi.height = new_h
-
- return new_roi
-
-
-class ScreenProcessor:
- """Class for doing tracking and converting screen to display and display to screen"""
-
- def __init__(self):
- pass
-
- # RUN FEATURE
- def process(self, point, point_offset, buffor_length, roi, edges, screen, display, heatmap):
- """Function performing main processing for screen processor. Tons of parameters doing brrrr"""
-
- s_point_offset = self.display2screen(point_offset, screen, display)
- p_on_display = self.screen2display([point[0] + s_point_offset[0], point[1] + s_point_offset[1]], roi, display)
- # p_on_display = self.screen2display(point,roi,display)
-
- if buffor_length > 20:
- new_edges = detect_edges(roi, display, point, p_on_display)
- edges.x = new_edges.x
- edges.y = new_edges.y
- edges.width = new_edges.width
- edges.height = new_edges.height
-
- p_on_display = (
- p_on_display[0] + display.offset_x,
- p_on_display[1] + display.offset_y,
- )
-
- # return how close that is hist region
- (_, _, roi_w, roi_h) = heatmap.getBoundaries()
- closeness_percentage = (roi_w * roi_h) / (screen.width * screen.height)
- return (p_on_display, closeness_percentage)
-
- # CALIBRATION FEATURE
- # it should not affect objects but produce new ones
- def update(self, roi, edges, cluster, heatmap):
- """Function to update screen processor with new clusters and heatmaps"""
-
- (x, y) = heatmap.getCenter()
- # =====================================
- # ---------histogram obtained----------
- # =====================================
- new_roi = dp.ScreenROI(roi.x, roi.y, roi.width, roi.height)
-
- new_roi.setCenter(x, y)
- edges.setCenter(x, y)
- # self.eyeScreen.setCenter(x,y)
- new_roi = scaleUp(new_roi, edges, scale=0.1)
-
- # =====================================
- # if screen is bigger than edges make it smaller
- # =====================================
- new_roi = scaleDown(new_roi, cluster, scale=-0.1)
- return new_roi
-
- def screen2display(self, screen_point, screen, display):
- """Function converting screen points to display points"""
-
- s_x, s_y = screen_point[0], screen_point[1]
-
- d_x = int((s_x - screen.x) / screen.width * display.width)
- d_y = int((s_y - screen.y) / screen.height * display.height)
-
- d_x = max(d_x, 0)
- d_y = max(d_y, 0)
- d_x = min(d_x, display.width)
- d_y = min(d_y, display.height)
-
- return (d_x, d_y)
-
- def display2screen(self, display_point, screen, display):
- """Function converting display points to screen points"""
-
- d_x, d_y = display_point[0], display_point[1]
-
- s_x = int((d_x) / display.width * screen.width)
- s_y = int((d_y) / display.height * screen.height)
-
- return (s_x, s_y)
-
-
-class ScreenManager:
- """Class performing most of tracking after estimating position of gaze"""
-
- def __init__(
- self,
- ):
- self.screen_processor = ScreenProcessor()
-
- def process(self, buffor, roi, edges, screen, display, calibration, offset):
- """Function doing processing and tracking and calibration of tracker"""
-
- heatmap = Heatmap(screen.width, screen.height, buffor.getBuffor())
- cluster = Clusters(buffor.getBuffor()).getMainCluster()
-
- if cluster is not None:
-
- if calibration:
- roi = self.screen_processor.update(roi, edges, cluster, heatmap)
-
- p, _ = self.screen_processor.process(
- buffor.getAvg(20),
- (offset[0], offset[1]),
- len(buffor.getBuffor()),
- roi,
- edges,
- screen,
- display,
- heatmap,
- )
-
- return (p, roi, cluster)
- return ([0, 0], roi, cluster)
diff --git a/eyeGestures/screenTracker_test.py b/eyeGestures/screenTracker_test.py
deleted file mode 100644
index 6030ae2..0000000
--- a/eyeGestures/screenTracker_test.py
+++ /dev/null
@@ -1,116 +0,0 @@
-import eyeGestures.screenTracker.dataPoints as dp
-import eyeGestures.screenTracker.screenTracker as scrtr
-
-# virtual tracking ScreenProcessor
-SCREEN_WIDTH = 500
-SCREEN_HEIGHT = 500
-
-# region of interest inside the virtual ScreenProcessor
-ROI_WIDTH = 50
-ROI_HEIGHT = 50
-
-# size of real display
-DISPLAY_WIDTH = 1200
-DISPLAY_HEIGHT = 1800
-
-####################### ScreenProcessor 2 display Tests #################################
-
-
-def test_screen2display_1_1():
- """[TEST]"""
- point = (1, 1)
-
- roi = dp.ScreenROI(0, 0, ROI_WIDTH, ROI_HEIGHT)
- display = dp.Display(DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0)
-
- tracker = scrtr.ScreenProcessor()
- p = tracker.screen2display(point, roi, display)
-
- assert p == (
- point[0] / ROI_WIDTH * DISPLAY_WIDTH,
- point[1] / ROI_HEIGHT * DISPLAY_HEIGHT,
- )
-
-
-def test_screen2display_55_1():
- """[TEST]"""
- point = (55, 1)
-
- roi = dp.ScreenROI(0, 0, ROI_WIDTH, ROI_HEIGHT)
- display = dp.Display(DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0)
-
- tracker = scrtr.ScreenProcessor()
- p = tracker.screen2display(point, roi, display)
-
- assert p == (DISPLAY_WIDTH, point[1] / ROI_HEIGHT * DISPLAY_HEIGHT)
-
-
-def test_screen2display_55_55():
- """[TEST]"""
- point = (55, 55)
-
- roi = dp.ScreenROI(0, 0, ROI_WIDTH, ROI_HEIGHT)
- display = dp.Display(DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0)
-
- tracker = scrtr.ScreenProcessor()
- p = tracker.screen2display(point, roi, display)
-
- assert p == (DISPLAY_WIDTH, DISPLAY_HEIGHT)
-
-
-def test_screen2display_0_0():
- """[TEST]"""
- point = (0, 0)
-
- roi = dp.ScreenROI(0, 0, ROI_WIDTH, ROI_HEIGHT)
- display = dp.Display(DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0)
-
- tracker = scrtr.ScreenProcessor()
- p = tracker.screen2display(point, roi, display)
-
- assert p == (0, 0)
-
-
-def test_screen2display_1_1_pos_50_50():
- """[TEST]"""
- point = (51, 51)
- pos = (50, 50)
-
- roi = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH, ROI_HEIGHT)
- display = dp.Display(DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0)
-
- tracker = scrtr.ScreenProcessor()
- p = tracker.screen2display(point, roi, display)
-
- assert p == (
- (point[0] - pos[0]) / ROI_WIDTH * DISPLAY_WIDTH,
- (point[1] - pos[1]) / ROI_HEIGHT * DISPLAY_HEIGHT,
- )
-
-
-def test_screen2display_55_55_pos_50_50():
- """[TEST]"""
- point = (105, 105)
- pos = (50, 50)
-
- roi = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH, ROI_HEIGHT)
- display = dp.Display(DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0)
-
- tracker = scrtr.ScreenProcessor()
- p = tracker.screen2display(point, roi, display)
-
- assert p == (DISPLAY_WIDTH, DISPLAY_HEIGHT)
-
-
-def test_screen2display_0_0_pos_50_50():
- """[TEST]"""
- point = (0, 0)
- pos = (50, 50)
-
- roi = dp.ScreenROI(pos[0], pos[1], ROI_WIDTH, ROI_HEIGHT)
- display = dp.Display(DISPLAY_WIDTH, DISPLAY_HEIGHT, 0, 0)
-
- tracker = scrtr.ScreenProcessor()
- p = tracker.screen2display(point, roi, display)
-
- assert p == (0, 0)
diff --git a/pybuild.bat b/pybuild.bat
index 5e8a7c6..f247dd6 100644
--- a/pybuild.bat
+++ b/pybuild.bat
@@ -2,4 +2,5 @@
@REM pyinstaller ./apps/app_win.spec
@REM deactivate;
-python -m build
+uv run --no-sync --python 3.13 --with build python -m build --wheel
+uv run --no-sync --python 3.14 --with build python -m build --wheel
\ No newline at end of file
diff --git a/pyproject.toml b/pyproject.toml
index db66e9f..2aa061a 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -1,48 +1,10 @@
[build-system]
-requires = ["hatchling"]
-build-backend = "hatchling.build"
-
-[tool.hatch.build.targets.sdist]
-
-dependencies = [
- "beautifulsoup4>=4.12.3",
- "keyboard>=0.13.5",
- "matplotlib>=3.8.2",
- "mediapipe>=0.10.8",
- "numpy>=1.26.4",
- "Pillow>=10.3.0",
- "PyAutoGUI>=0.9.54",
- "pycocotools>=2.0.7",
- "pygame>=2.5.2",
- "pynput>=1.7.6",
- "pytest>=7.4.2",
- "scikit_learn>=1.3.2",
- "scipy>=1.12.0",
- "screeninfo>=0.8.1",
- "setuptools>=70.0.0",
- "scipy>=1.12.0",
- "opencv-contrib-python>=4.9.0.80",
- "xlwt>=1.3.0"
-]
-
-include = [
- "eyeGestures/*.py",
- "eyeGestures/screenTracker/*.py",
- "eyeGestures/eyegestures.py"
-]
-
-exclude = [
- "*.json",
- "lab/*",
- "app.py",
- "test_app.py",
- "test.py",
- "tools/*"
-]
+requires = ["maturin>=1.13,<2.0"]
+build-backend = "maturin"
[project]
name = "eyeGestures"
-version = "3.2.4"
+version = "4.0.0"
authors = [
{ name="Piotr Walas", email="piotr.walas@eyegestures.com" },
]
@@ -52,13 +14,26 @@ maintainers = [
description = "Package for eye tracking algorithm allowing for development of gaze controlled computer interface"
readme = "README.md"
license = {file = "LICENSE"}
-requires-python = ">=3.8"
+requires-python = ">=3.13"
keywords = ["eyetracking", "eye", "gaze", "tracking", "gazetracking"]
classifiers = [
"Programming Language :: Python :: 3",
"License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
"Operating System :: OS Independent",
]
+dependencies = [
+ "black>=26.5.1",
+ "isort>=8.0.1",
+ "mediapipe>=0.10.35",
+ "opencv-python>=4.13.0.92",
+]
+
+[tool.maturin]
+manifest-path = "engine/Cargo.toml"
+features = ["python"]
+no-default-features = true
+python-source = "."
+module-name = "eyeGestures.EyegesturesEngine"
[project.urls]
Homepage = "https://github.com/NativeSensors/EyeGestures"
@@ -106,8 +81,11 @@ ignore = "DAR101, DAR201, E203, E266, E712, W503"
[tool.mypy]
strict = false
-plugins = ["numpy.typing.mypy_plugin"]
[[tool.mypy.overrides]]
module = ["sklearn.*", "mediapipe.*"]
-ignore_missing_imports = true
\ No newline at end of file
+ignore_missing_imports = true
+
+[[tool.mypy.overrides]]
+module = ["EyegesturesEngine", "eyeGestures.EyegesturesEngine"]
+ignore_missing_imports = true
diff --git a/scripts/csv_reader.py b/scripts/csv_reader.py
deleted file mode 100644
index 77c378d..0000000
--- a/scripts/csv_reader.py
+++ /dev/null
@@ -1,48 +0,0 @@
-import pickle
-import csv
-import ast
-
-def read_gaze_data_from_csv(filename):
- """Reads gaze event data from a CSV file.
-
- Args:
- filename (str): Name of the CSV file to read data from.
-
- Returns:
- list: List of dictionaries containing gaze event data.
- """
-
- gaze_data = []
-
- with open(filename, newline='') as csvfile:
- reader = csv.reader(csvfile)
- for n,row in enumerate(reader):
- if n > 0:
- print(n,row[6],type(row[6]),row[6][2])
- print(bytearray(f'{row[6][2:-3]}', 'latin-1'))
- # Deserialize the string representation of arrays
-
- row_data = {
- "point_x": row[0],
- "point_y": row[1],
- "blink": row[2],
- "fixation": row[3],
- "screen_x": row[4],
- "screen_y": row[5],
- "l_eye_landmarks": pickle.loads(bytearray(f'{row[6][2:-1]}', 'latin-1').decode('unicode_escape').encode('latin1')), # Deserialize the string representation of the array
- "r_eye_landmarks": pickle.loads(bytearray(f'{row[7][2:-1]}', 'latin-1').decode('unicode_escape').encode('latin1')), # Deserialize the string representation of the array
- "l_eye_pupil": pickle.loads(bytearray(f'{row[8][2:-1]}', 'latin-1').decode('unicode_escape').encode('latin1')), # Deserialize the string representation of the array
- "r_eye_pupil": pickle.loads(bytearray(f'{row[9][2:-1]}', 'latin-1').decode('unicode_escape').encode('latin1')), # Deserialize the string representation of the array
- "screen_width": row[10],
- "screen_height": row[11],
- "rois": row[12] # Deserialize the string representation of the array
- }
- gaze_data.append(row_data)
-
- return gaze_data
-
-# Example usage:
-filename = 'collection_91675a1d.csv'
-gaze_data = read_gaze_data_from_csv(filename)
-for data in gaze_data:
- print(data)
\ No newline at end of file
diff --git a/scripts/heatmap_maker.py b/scripts/heatmap_maker.py
deleted file mode 100644
index 4be7384..0000000
--- a/scripts/heatmap_maker.py
+++ /dev/null
@@ -1,123 +0,0 @@
-import matplotlib.pyplot as plt
-import numpy as np
-import argparse
-import pickle
-import csv
-import ast
-import re
-import os
-
-def read_gaze_data_from_csv(filename):
- """Reads gaze event data from a CSV file.
-
- Args:
- filename (str): Name of the CSV file to read data from.
-
- Returns:
- list: List of dictionaries containing gaze event data.
- """
-
- gaze_data = []
-
- with open(filename, newline='') as csvfile:
- reader = csv.reader(csvfile)
- for n,row in enumerate(reader):
- if n > 0:
- # print(n,row[6],type(row[6]),row[6][2])
- # print(bytearray(f'{row[6][2:-3]}', 'latin-1'))
- # Deserialize the string representation of arrays
-
- row_data = {
- "timestamp": row[0],
- "point_x": row[1],
- "point_y": row[2],
- "blink": row[3],
- "fixation": row[4],
- "screen_x": row[5],
- "screen_y": row[6],
- "screen_width": row[7],
- "screen_height": row[8],
- "rois": row[12] # Deserialize the string representation of the array
- }
- gaze_data.append(row_data)
-
- return gaze_data
-
-def draw_heatmap(data_points,x_dim,y_dim,picture_path = None):
- x_range = [0,x_dim]
- y_range = [0,y_dim]
- heatmap, xedges, yedges = np.histogram2d(data_points[:,0], data_points[:,1], bins = 50, range=[x_range, y_range])
- extent = [xedges[0], xedges[-1], yedges[-1], yedges[0]]
- alpha = 1.0
-
- plt.clf()
- if picture_path:
- map_image = plt.imread(picture_path)
- map_image = np.flipud(map_image)
- alpha = 0.5
- plt.imshow(map_image, extent=extent, aspect='auto')
-
-
- # Plot the image as background
- plt.imshow(heatmap.T, extent=extent, origin='lower', alpha=alpha, cmap='hot')
- plt.gca().invert_yaxis()
- plt.show()
-
-def main(gaze_data_file,background_file = False, step = None, window_size = None, start = None, stop = None):
-
- # Example usage:
- # filename = 'data/collection_73cbc017/data.csv'
- # picture = 'data/collection_73cbc017/recordings/1713258117.949606.png'
-
- # picture = background_file
-
- recordings_path = f"{os.path.dirname(gaze_data_file)}/recordings"
-
- filename = gaze_data_file
- gaze_data = read_gaze_data_from_csv(filename)
- data_points = []
- timestamps = []
- screen_w = float(gaze_data[0]["screen_width"])
- screen_h = float(gaze_data[0]["screen_height"])
-
- for data in gaze_data:
- timestamps.append(float(data["timestamp"]))
- data_points.append([float(data["screen_x"]),float(data["screen_y"])])
- data_points = np.array(data_points)
-
- if start:
- index = timestamps.index(start)
- timestamps = timestamps[index:]
- data_points = data_points[index:,:]
-
- if stop:
- index = timestamps.index(stop)
- timestamps = timestamps[:index]
- data_points = data_points[:index,:]
-
- if window_size == None or step == None:
- picture_path = None
- if background_file:
- picture_path = f"{recordings_path}/{timestamps[0]}.png"
- draw_heatmap(data_points,screen_w,screen_h,picture_path)
- else:
- for i in range(0,len(timestamps),step):
- picture_path = None
- if background_file:
- print(f"{background_file}")
- picture_path = f"{recordings_path}/{timestamps[i]}.png"
- data_points_range = data_points[i:i+window_size]
- draw_heatmap(data_points_range,screen_w,screen_h,picture_path)
-
-if __name__ == "__main__":
- parser = argparse.ArgumentParser(description='Generate heatmap based on gaze data over an image.')
- parser.add_argument('path_to_gaze_data', type=str, help='Path to the gaze data file')
- parser.add_argument('--window', type=int, help='Window size in samples')
- parser.add_argument('--step', type=int, help='Window size in samples')
- parser.add_argument('--background', action='store_true', help='Path to the background file')
- parser.add_argument('--start', type=float, help='Window size in samples')
- parser.add_argument('--stop', type=float, help='Path to the background file')
-
- args = parser.parse_args()
-
- main(args.path_to_gaze_data,background_file = args.background, step = args.step, window_size = args.window, start = args.start, stop = args.stop)
diff --git a/tests/__init__.py b/tests/__init__.py
deleted file mode 100644
index e69de29..0000000
diff --git a/tests/data/face_1.jpg b/tests/data/face_1.jpg
deleted file mode 100644
index 0d3a0e1..0000000
Binary files a/tests/data/face_1.jpg and /dev/null differ
diff --git a/tests/data/face_2.jpg b/tests/data/face_2.jpg
deleted file mode 100644
index a879c02..0000000
Binary files a/tests/data/face_2.jpg and /dev/null differ
diff --git a/tests/test_gestures.py b/tests/test_gestures.py
deleted file mode 100644
index 6269862..0000000
--- a/tests/test_gestures.py
+++ /dev/null
@@ -1,65 +0,0 @@
-import os
-import sys
-import cv2
-import unittest
-import numpy as np
-from eyeGestures.utils import VideoCapture
-from eyeGestures import EyeGestures_v3
-
-class TestBasic(unittest.TestCase):
-
-
- def test_basic_run(self):
- gestures = EyeGestures_v3()
- # Load image (color)
- frame = cv2.imread("tests/data/face_2.jpg")
- calibrate = True
- screen_width = 100
- screen_height = 100
-
- x = np.arange(0, 1.1, 0.2)
- y = np.arange(0, 1.1, 0.2)
-
- xx, yy = np.meshgrid(x, y)
- calibration_map = np.column_stack([xx.ravel(), yy.ravel()])
- gestures.uploadCalibrationMap(calibration_map)
-
- event, calibration = gestures.step(frame, calibrate, screen_width, screen_height)
-
- self.assertNotEqual(event, None)
- self.assertNotEqual(calibration, None)
-
- def test_basic_run_calibration_finished(self):
- gestures = EyeGestures_v3()
- # Load image (color)
- frame = cv2.imread("tests/data/face_2.jpg")
-
- calibrate = True
- screen_width = 100
- screen_height = 100
-
- x = np.arange(0, 1.1, 1.0)
- y = np.arange(0, 1.1, 1.0)
-
- xx, yy = np.meshgrid(x, y)
- calibration_map = np.column_stack([xx.ravel(), yy.ravel()])
- gestures.uploadCalibrationMap(calibration_map)
-
- for _ in range(500):
- event, calibration = gestures.step(frame, calibrate, screen_width, screen_height)
-
- calibrate = False
- event, calibration = gestures.step(frame, calibrate, screen_width, screen_height)
-
- self.assertNotEqual(event, None)
- self.assertNotEqual(calibration, None)
-
- def test_upload_calibration_mask(self):
- gestures = EyeGestures_v3()
-
- x = np.arange(0, 1.1, 0.2)
- y = np.arange(0, 1.1, 0.2)
-
- xx, yy = np.meshgrid(x, y)
- calibration_map = np.column_stack([xx.ravel(), yy.ravel()])
- gestures.uploadCalibrationMap(calibration_map)
diff --git a/tools/find_direction_vector.py b/tools/find_direction_vector.py
deleted file mode 100644
index cf396eb..0000000
--- a/tools/find_direction_vector.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import numpy as np
-import matplotlib
-matplotlib.use('TkAgg') # Set the backend to TkAgg
-import matplotlib.pyplot as plt
-
-
-points = np.array([[222, 133],
- [200, 130],
- [201, 129],
- [210, 127],
- [207, 134],
- [225, 131],
- [216, 127],
- [204, 133],
- [217, 134],
- [202, 131],
- [203, 128],
- [221, 129],
- [200, 130],
- [206, 128],
- [212, 135],
- [225, 133]])
-
-min_x = np.min(points[:,0])
-max_x = np.max(points[:,0])
-min_y = np.min(points[:,1])
-max_y = np.max(points[:,1])
-
-center = np.array(((max_x+min_x)/2,(max_y+min_y)/2))
-# print(min_x,max_x,center)
-
-points = points - center
-center = center - center
-
-# Function to generate random points on an ellipse
-def generate_ellipse_points(center, a, b, n=20):
- angles = np.linspace(0, 2 * np.pi, n)
- return np.array([[center[0] + a * np.cos(angle), center[1] + b * np.sin(angle)] for angle in angles])
-
-# Function to calculate the corrected gaze vector
-def calculate_corrected_gaze_vector(random_point, points):
- vectors = points - random_point
- # print(vectors)
- distances = np.linalg.norm(vectors, axis=1)
- # print(distances)
- weights = np.exp(-distances) # Exponential weighting based on distance
- # print(weights)
- weighted_vectors = vectors * weights[:, None]
- gaze_vector = np.sum(weighted_vectors, axis=0)
- # print(gaze_vector/1000)
- return gaze_vector * 100
-
-# Function to check if a point is inside the ellipse
-def is_point_inside_ellipse(point, center, a, b):
- # Adjust point based on the center of ellipse
- px, py = point[0] - center[0], point[1] - center[1]
- # Check the ellipse equation (x^2/a^2 + y^2/b^2 <= 1)
- return (px**2) / (a**2) + (py**2) / (b**2) <= 1
-
-def calculate_vector(random_point, a, b):
-
- # Example ellipse parameters
- # center = np.array([0, 0])
-
- # Generate points on the ellipse
- # points = generate_ellipse_points(center, a, b)
-
- # Generate a random point inside the ellipse
-
-
- # Calculate the gaze vector
- gaze_vector = calculate_corrected_gaze_vector(random_point, points)
-
-
- # Normalize and extend the gaze vector for better visibility
- gaze_vector_normalized = gaze_vector / np.linalg.norm(gaze_vector)
- gaze_vector_extended = gaze_vector_normalized
-
- return gaze_vector_extended
-
-# Plotting
-fig, ax = plt.subplots()
-
-a, b = 5, 3 # Semi-major and semi-minor axes of the ellipse
-# Example ellipse parameters
-# center = np.array([0, 0])
-
-# Generate points on the ellipse
-# points = generate_ellipse_points(center, a, b)
-random_point = center
-
-
-plt.scatter(points[:, 0], points[:, 1], color='blue', label='Elliptic Points')
-point, = ax.plot(random_point[0], random_point[1], color='orange', label='Random Point')
-r_vector, = ax.plot([0], [0], 'g-')
-
-# Event handler to update the point position
-def on_move(event):
- if event.inaxes == ax:
-
- a, b = 5, 3 # Semi-major and semi-minor axes of the ellipse
- random_point = np.array([event.xdata,event.ydata])
-
- gaze_vector_extended = calculate_vector(random_point,a,b)
-
- point.set_data(random_point[0], random_point[1])
- r_vector.set_data([random_point[0], random_point[0] + gaze_vector_extended[0]],
- [random_point[1], random_point[1] + gaze_vector_extended[1]])
-
-
- fig.canvas.draw()
-
-
-# Connect the event handler to the figure
-fig.canvas.mpl_connect('motion_notify_event', on_move)
-
-plt.axhline(0, color='black', linewidth=0.5)
-plt.axvline(0, color='black', linewidth=0.5)
-plt.gca().set_aspect('equal', adjustable='box')
-plt.title("Corrected Gaze Vector Calculation")
-plt.xlabel("X-axis")
-plt.ylabel("Y-axis")
-plt.legend()
-plt.grid(True)
-plt.show()
diff --git a/tools/pupilDetection.py b/tools/pupilDetection.py
deleted file mode 100644
index 1c6ae12..0000000
--- a/tools/pupilDetection.py
+++ /dev/null
@@ -1,43 +0,0 @@
-import cv2
-from eyeGestures.utils import VideoCapture
-from eyeGestures.face import FaceFinder
-
-def showEyes(image,face):
-
- if face is not None:
- cv2.circle(frame,face.getLeftPupil(),2,(0,0,255),1)
- for point in face.getLeftEye():
- cv2.circle(frame,point,2,(0,255,0),1)
-
- cv2.circle(frame,face.getRightPupil(),2,(0,0,255),1)
- for point in face.getRightEye():
- cv2.circle(frame,point,2,(0,255,0),1)
-
-
-if __name__ == "__main__":
- vid = VideoCapture('recording/calibrationData1.pkl')
- # vid = VideoCapture('rtsp://192.168.18.30:8080/h264.sdp')
-
- faceFinder = FaceFinder()
-
- # use half of the data for fitting
- pFrame = None
- flow = None
- ret = True
- while ret:
-
- ret, frame = vid.read()
- gray = cv2.cvtColor(frame,cv2.COLOR_BGR2GRAY)
-
- # detect face
- face = faceFinder.find(gray)
-
- #display face
- showEyes(frame,face)
- cv2.imshow("frame",frame)
-
- if cv2.waitKey(10) == ord('q'):
- run = False
- break
-
- cv2.destroyAllWindows()
\ No newline at end of file
diff --git a/uv.lock b/uv.lock
new file mode 100644
index 0000000..42d5af3
--- /dev/null
+++ b/uv.lock
@@ -0,0 +1,640 @@
+version = 1
+revision = 3
+requires-python = ">=3.13"
+
+[[package]]
+name = "absl-py"
+version = "2.4.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/64/c7/8de93764ad66968d19329a7e0c147a2bb3c7054c554d4a119111b8f9440f/absl_py-2.4.0.tar.gz", hash = "sha256:8c6af82722b35cf71e0f4d1d47dcaebfff286e27110a99fc359349b247dfb5d4", size = 116543, upload-time = "2026-01-28T10:17:05.322Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/18/a6/907a406bb7d359e6a63f99c313846d9eec4f7e6f7437809e03aa00fa3074/absl_py-2.4.0-py3-none-any.whl", hash = "sha256:88476fd881ca8aab94ffa78b7b6c632a782ab3ba1cd19c9bd423abc4fb4cd28d", size = 135750, upload-time = "2026-01-28T10:17:04.19Z" },
+]
+
+[[package]]
+name = "black"
+version = "26.5.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "click" },
+ { name = "mypy-extensions" },
+ { name = "packaging" },
+ { name = "pathspec" },
+ { name = "platformdirs" },
+ { name = "pytokens" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/c0/37/5628dd55bf2b34257fc7603f0fe97c40e3aaf24265f416a9c85c95ca1436/black-26.5.1.tar.gz", hash = "sha256:dd321f668053961824bcc1be1cc1df748b2d7e4fa28086b08331e577b0100a73", size = 679439, upload-time = "2026-05-18T16:53:36.107Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3f/5c/c384363980e11e25ca6b93205949bb331fbf35f4e0dbec376dfa6326cec8/black-26.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2b36cf2ddf5566e205f6535f782a62194a184d33e175b64ae8c40b1737522be3", size = 2009020, upload-time = "2026-05-18T17:05:28.132Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/df/9f31c5e0babbfed77d505fc5d120beb98b21b33feaeded3924ea941fe360/black-26.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1f7ea64ebfa01b50f693508fc39f875e264446d3b097088f84f203b9d09618a0", size = 1813335, upload-time = "2026-05-18T17:05:31.266Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/24/8e7b9a2fa61b0afd82209efe937557d180a1fa055bd7f6161eb9defc3719/black-26.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecb3e624844c798144e9bd986954e0adc81d8911a1f30f375e1252fe26e8c294", size = 1881614, upload-time = "2026-05-18T17:05:32.718Z" },
+ { url = "https://files.pythonhosted.org/packages/49/ad/b4e0d9365ba8ac34f6bbab62a4b1b2dd5d618fac3fa1b8db968c844201b5/black-26.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:e1a26503279b6b310669fb0b219c39e4820b77e8189fe80f522bb511f247db0a", size = 1488925, upload-time = "2026-05-18T17:05:34.259Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/4b/652b859bf5df88a751c30451b09338f7fd26a77d1271c666992f836b7711/black-26.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c34b25da232ead53a6f335b76dbea124f4d152ad568b9080d6f944bc2b34b52", size = 1289883, upload-time = "2026-05-18T17:05:36.019Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/16/a8da8eb208c51c7f4ce74609a45d0dcc6d8a2141e45e81ee5289d1bb0d59/black-26.5.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e88976690a64b0af98312ca958415849cb42423423c5f2ee74af4b49a97a2168", size = 2004800, upload-time = "2026-05-18T17:05:38.182Z" },
+ { url = "https://files.pythonhosted.org/packages/11/8a/a479296a19e383b70a725882a6cf3d786540601ff03cabbaaf1cce864c5a/black-26.5.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:32d5ea7f6c8bdfa6e648326ebca1f02b0764e2a029edc6f8dce2627e19d468c3", size = 1815576, upload-time = "2026-05-18T17:05:40.309Z" },
+ { url = "https://files.pythonhosted.org/packages/81/6b/cfaf3d39f25132c156a068f6b805576c9103a84086019507c70e1911ee7d/black-26.5.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ea8d16dc41655aa113cd64665e7219446cd7e4ff2248d7178eaa905190c86b18", size = 1877927, upload-time = "2026-05-18T17:05:42.463Z" },
+ { url = "https://files.pythonhosted.org/packages/66/76/302e313964bcff7e28df329d39f84f5270095730d85ff0acc260610a0d82/black-26.5.1-cp314-cp314-win_amd64.whl", hash = "sha256:577f21094ea469ef92ec1adaf2c9441a226d2144d01a5be2fa823cecf6543e50", size = 1511860, upload-time = "2026-05-18T17:05:43.943Z" },
+ { url = "https://files.pythonhosted.org/packages/27/4e/a3827e35e0e567f9f9ee59e2a0ab979267dca98718f25547ca8c6733afd4/black-26.5.1-cp314-cp314-win_arm64.whl", hash = "sha256:ed1a20af114c301a0269bf01163d51dbef72737fd65f850001e7cbe7f3c7abae", size = 1316632, upload-time = "2026-05-18T17:05:45.521Z" },
+ { url = "https://files.pythonhosted.org/packages/94/51/f975cae76d44274cc2868dc9040ac5d58d464784610234455b4e7b19c6ef/black-26.5.1-py3-none-any.whl", hash = "sha256:4ed7f7da04046d2e488437170797d3b4a4ad83906683bcb7dfc68b673bbce5e2", size = 213693, upload-time = "2026-05-18T16:53:33.964Z" },
+]
+
+[[package]]
+name = "certifi"
+version = "2026.5.20"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
+]
+
+[[package]]
+name = "cffi"
+version = "2.0.0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "pycparser", marker = "implementation_name != 'PyPy'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4b/8d/a0a47a0c9e413a658623d014e91e74a50cdd2c423f7ccfd44086ef767f90/cffi-2.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:00bdf7acc5f795150faa6957054fbbca2439db2f775ce831222b66f192f03beb", size = 185230, upload-time = "2025-09-08T23:23:00.879Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/d2/a6c0296814556c68ee32009d9c2ad4f85f2707cdecfd7727951ec228005d/cffi-2.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:45d5e886156860dc35862657e1494b9bae8dfa63bf56796f2fb56e1679fc0bca", size = 181043, upload-time = "2025-09-08T23:23:02.231Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/1e/d22cc63332bd59b06481ceaac49d6c507598642e2230f201649058a7e704/cffi-2.0.0-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:07b271772c100085dd28b74fa0cd81c8fb1a3ba18b21e03d7c27f3436a10606b", size = 212446, upload-time = "2025-09-08T23:23:03.472Z" },
+ { url = "https://files.pythonhosted.org/packages/a9/f5/a2c23eb03b61a0b8747f211eb716446c826ad66818ddc7810cc2cc19b3f2/cffi-2.0.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d48a880098c96020b02d5a1f7d9251308510ce8858940e6fa99ece33f610838b", size = 220101, upload-time = "2025-09-08T23:23:04.792Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/7f/e6647792fc5850d634695bc0e6ab4111ae88e89981d35ac269956605feba/cffi-2.0.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:f93fd8e5c8c0a4aa1f424d6173f14a892044054871c771f8566e4008eaa359d2", size = 207948, upload-time = "2025-09-08T23:23:06.127Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/1e/a5a1bd6f1fb30f22573f76533de12a00bf274abcdc55c8edab639078abb6/cffi-2.0.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:dd4f05f54a52fb558f1ba9f528228066954fee3ebe629fc1660d874d040ae5a3", size = 206422, upload-time = "2025-09-08T23:23:07.753Z" },
+ { url = "https://files.pythonhosted.org/packages/98/df/0a1755e750013a2081e863e7cd37e0cdd02664372c754e5560099eb7aa44/cffi-2.0.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c8d3b5532fc71b7a77c09192b4a5a200ea992702734a2e9279a37f2478236f26", size = 219499, upload-time = "2025-09-08T23:23:09.648Z" },
+ { url = "https://files.pythonhosted.org/packages/50/e1/a969e687fcf9ea58e6e2a928ad5e2dd88cc12f6f0ab477e9971f2309b57c/cffi-2.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d9b29c1f0ae438d5ee9acb31cadee00a58c46cc9c0b2f9038c6b0b3470877a8c", size = 222928, upload-time = "2025-09-08T23:23:10.928Z" },
+ { url = "https://files.pythonhosted.org/packages/36/54/0362578dd2c9e557a28ac77698ed67323ed5b9775ca9d3fe73fe191bb5d8/cffi-2.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6d50360be4546678fc1b79ffe7a66265e28667840010348dd69a314145807a1b", size = 221302, upload-time = "2025-09-08T23:23:12.42Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/6d/bf9bda840d5f1dfdbf0feca87fbdb64a918a69bca42cfa0ba7b137c48cb8/cffi-2.0.0-cp313-cp313-win32.whl", hash = "sha256:74a03b9698e198d47562765773b4a8309919089150a0bb17d829ad7b44b60d27", size = 172909, upload-time = "2025-09-08T23:23:14.32Z" },
+ { url = "https://files.pythonhosted.org/packages/37/18/6519e1ee6f5a1e579e04b9ddb6f1676c17368a7aba48299c3759bbc3c8b3/cffi-2.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:19f705ada2530c1167abacb171925dd886168931e0a7b78f5bffcae5c6b5be75", size = 183402, upload-time = "2025-09-08T23:23:15.535Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/0e/02ceeec9a7d6ee63bb596121c2c8e9b3a9e150936f4fbef6ca1943e6137c/cffi-2.0.0-cp313-cp313-win_arm64.whl", hash = "sha256:256f80b80ca3853f90c21b23ee78cd008713787b1b1e93eae9f3d6a7134abd91", size = 177780, upload-time = "2025-09-08T23:23:16.761Z" },
+ { url = "https://files.pythonhosted.org/packages/92/c4/3ce07396253a83250ee98564f8d7e9789fab8e58858f35d07a9a2c78de9f/cffi-2.0.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fc33c5141b55ed366cfaad382df24fe7dcbc686de5be719b207bb248e3053dc5", size = 185320, upload-time = "2025-09-08T23:23:18.087Z" },
+ { url = "https://files.pythonhosted.org/packages/59/dd/27e9fa567a23931c838c6b02d0764611c62290062a6d4e8ff7863daf9730/cffi-2.0.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c654de545946e0db659b3400168c9ad31b5d29593291482c43e3564effbcee13", size = 181487, upload-time = "2025-09-08T23:23:19.622Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/43/0e822876f87ea8a4ef95442c3d766a06a51fc5298823f884ef87aaad168c/cffi-2.0.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:24b6f81f1983e6df8db3adc38562c83f7d4a0c36162885ec7f7b77c7dcbec97b", size = 220049, upload-time = "2025-09-08T23:23:20.853Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/89/76799151d9c2d2d1ead63c2429da9ea9d7aac304603de0c6e8764e6e8e70/cffi-2.0.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:12873ca6cb9b0f0d3a0da705d6086fe911591737a59f28b7936bdfed27c0d47c", size = 207793, upload-time = "2025-09-08T23:23:22.08Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/dd/3465b14bb9e24ee24cb88c9e3730f6de63111fffe513492bf8c808a3547e/cffi-2.0.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:d9b97165e8aed9272a6bb17c01e3cc5871a594a446ebedc996e2397a1c1ea8ef", size = 206300, upload-time = "2025-09-08T23:23:23.314Z" },
+ { url = "https://files.pythonhosted.org/packages/47/d9/d83e293854571c877a92da46fdec39158f8d7e68da75bf73581225d28e90/cffi-2.0.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:afb8db5439b81cf9c9d0c80404b60c3cc9c3add93e114dcae767f1477cb53775", size = 219244, upload-time = "2025-09-08T23:23:24.541Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0f/1f177e3683aead2bb00f7679a16451d302c436b5cbf2505f0ea8146ef59e/cffi-2.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:737fe7d37e1a1bffe70bd5754ea763a62a066dc5913ca57e957824b72a85e205", size = 222828, upload-time = "2025-09-08T23:23:26.143Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/0f/cafacebd4b040e3119dcb32fed8bdef8dfe94da653155f9d0b9dc660166e/cffi-2.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:38100abb9d1b1435bc4cc340bb4489635dc2f0da7456590877030c9b3d40b0c1", size = 220926, upload-time = "2025-09-08T23:23:27.873Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/aa/df335faa45b395396fcbc03de2dfcab242cd61a9900e914fe682a59170b1/cffi-2.0.0-cp314-cp314-win32.whl", hash = "sha256:087067fa8953339c723661eda6b54bc98c5625757ea62e95eb4898ad5e776e9f", size = 175328, upload-time = "2025-09-08T23:23:44.61Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/92/882c2d30831744296ce713f0feb4c1cd30f346ef747b530b5318715cc367/cffi-2.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:203a48d1fb583fc7d78a4c6655692963b860a417c0528492a6bc21f1aaefab25", size = 185650, upload-time = "2025-09-08T23:23:45.848Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/2c/98ece204b9d35a7366b5b2c6539c350313ca13932143e79dc133ba757104/cffi-2.0.0-cp314-cp314-win_arm64.whl", hash = "sha256:dbd5c7a25a7cb98f5ca55d258b103a2054f859a46ae11aaf23134f9cc0d356ad", size = 180687, upload-time = "2025-09-08T23:23:47.105Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/61/c768e4d548bfa607abcda77423448df8c471f25dbe64fb2ef6d555eae006/cffi-2.0.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:9a67fc9e8eb39039280526379fb3a70023d77caec1852002b4da7e8b270c4dd9", size = 188773, upload-time = "2025-09-08T23:23:29.347Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/ea/5f76bce7cf6fcd0ab1a1058b5af899bfbef198bea4d5686da88471ea0336/cffi-2.0.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7a66c7204d8869299919db4d5069a82f1561581af12b11b3c9f48c584eb8743d", size = 185013, upload-time = "2025-09-08T23:23:30.63Z" },
+ { url = "https://files.pythonhosted.org/packages/be/b4/c56878d0d1755cf9caa54ba71e5d049479c52f9e4afc230f06822162ab2f/cffi-2.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cc09976e8b56f8cebd752f7113ad07752461f48a58cbba644139015ac24954c", size = 221593, upload-time = "2025-09-08T23:23:31.91Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/0d/eb704606dfe8033e7128df5e90fee946bbcb64a04fcdaa97321309004000/cffi-2.0.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:92b68146a71df78564e4ef48af17551a5ddd142e5190cdf2c5624d0c3ff5b2e8", size = 209354, upload-time = "2025-09-08T23:23:33.214Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/19/3c435d727b368ca475fb8742ab97c9cb13a0de600ce86f62eab7fa3eea60/cffi-2.0.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:b1e74d11748e7e98e2f426ab176d4ed720a64412b6a15054378afdb71e0f37dc", size = 208480, upload-time = "2025-09-08T23:23:34.495Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/44/681604464ed9541673e486521497406fadcc15b5217c3e326b061696899a/cffi-2.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:28a3a209b96630bca57cce802da70c266eb08c6e97e5afd61a75611ee6c64592", size = 221584, upload-time = "2025-09-08T23:23:36.096Z" },
+ { url = "https://files.pythonhosted.org/packages/25/8e/342a504ff018a2825d395d44d63a767dd8ebc927ebda557fecdaca3ac33a/cffi-2.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7553fb2090d71822f02c629afe6042c299edf91ba1bf94951165613553984512", size = 224443, upload-time = "2025-09-08T23:23:37.328Z" },
+ { url = "https://files.pythonhosted.org/packages/e1/5e/b666bacbbc60fbf415ba9988324a132c9a7a0448a9a8f125074671c0f2c3/cffi-2.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:6c6c373cfc5c83a975506110d17457138c8c63016b563cc9ed6e056a82f13ce4", size = 223437, upload-time = "2025-09-08T23:23:38.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/1d/ec1a60bd1a10daa292d3cd6bb0b359a81607154fb8165f3ec95fe003b85c/cffi-2.0.0-cp314-cp314t-win32.whl", hash = "sha256:1fc9ea04857caf665289b7a75923f2c6ed559b8298a1b8c49e59f7dd95c8481e", size = 180487, upload-time = "2025-09-08T23:23:40.423Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/41/4c1168c74fac325c0c8156f04b6749c8b6a8f405bbf91413ba088359f60d/cffi-2.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:d68b6cef7827e8641e8ef16f4494edda8b36104d79773a334beaa1e3521430f6", size = 191726, upload-time = "2025-09-08T23:23:41.742Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/3a/dbeec9d1ee0844c679f6bb5d6ad4e9f198b1224f4e7a32825f47f6192b0c/cffi-2.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0a1527a803f0a659de1af2e1fd700213caba79377e27e4693648c2923da066f9", size = 184195, upload-time = "2025-09-08T23:23:43.004Z" },
+]
+
+[[package]]
+name = "click"
+version = "8.4.1"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "colorama", marker = "sys_platform == 'win32'" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
+]
+
+[[package]]
+name = "colorama"
+version = "0.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" },
+]
+
+[[package]]
+name = "contourpy"
+version = "1.3.3"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" },
+ { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" },
+ { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" },
+ { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" },
+ { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" },
+ { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" },
+ { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" },
+ { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" },
+ { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" },
+ { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" },
+ { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" },
+ { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" },
+ { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" },
+ { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" },
+ { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" },
+ { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" },
+ { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" },
+ { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" },
+ { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" },
+ { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" },
+ { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" },
+]
+
+[[package]]
+name = "cycler"
+version = "0.12.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" },
+]
+
+[[package]]
+name = "eyegestures"
+version = "4.0.0"
+source = { editable = "." }
+dependencies = [
+ { name = "black" },
+ { name = "isort" },
+ { name = "mediapipe" },
+ { name = "opencv-python" },
+]
+
+[package.metadata]
+requires-dist = [
+ { name = "black", specifier = ">=26.5.1" },
+ { name = "isort", specifier = ">=8.0.1" },
+ { name = "mediapipe", specifier = ">=0.10.35" },
+ { name = "opencv-python", specifier = ">=4.13.0.92" },
+]
+
+[[package]]
+name = "flatbuffers"
+version = "25.12.19"
+source = { registry = "https://pypi.org/simple" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" },
+]
+
+[[package]]
+name = "fonttools"
+version = "4.63.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" },
+ { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" },
+ { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" },
+ { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" },
+ { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" },
+ { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" },
+ { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" },
+ { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" },
+ { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" },
+ { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" },
+ { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" },
+ { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" },
+]
+
+[[package]]
+name = "isort"
+version = "8.0.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/ef/7c/ec4ab396d31b3b395e2e999c8f46dec78c5e29209fac49d1f4dace04041d/isort-8.0.1.tar.gz", hash = "sha256:171ac4ff559cdc060bcfff550bc8404a486fee0caab245679c2abe7cb253c78d", size = 769592, upload-time = "2026-02-28T10:08:20.685Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/3e/95/c7c34aa53c16353c56d0b802fba48d5f5caa2cdee7958acbcb795c830416/isort-8.0.1-py3-none-any.whl", hash = "sha256:28b89bc70f751b559aeca209e6120393d43fbe2490de0559662be7a9787e3d75", size = 89733, upload-time = "2026-02-28T10:08:19.466Z" },
+]
+
+[[package]]
+name = "kiwisolver"
+version = "1.5.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" },
+ { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" },
+ { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" },
+ { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" },
+ { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" },
+ { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" },
+ { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" },
+ { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" },
+ { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" },
+ { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" },
+ { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" },
+ { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" },
+ { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" },
+ { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" },
+ { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" },
+ { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" },
+ { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" },
+ { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" },
+ { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" },
+ { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" },
+ { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" },
+ { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" },
+ { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" },
+ { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" },
+ { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" },
+ { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" },
+ { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" },
+ { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" },
+ { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" },
+ { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" },
+ { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" },
+]
+
+[[package]]
+name = "matplotlib"
+version = "3.10.9"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "contourpy" },
+ { name = "cycler" },
+ { name = "fonttools" },
+ { name = "kiwisolver" },
+ { name = "numpy" },
+ { name = "packaging" },
+ { name = "pillow" },
+ { name = "pyparsing" },
+ { name = "python-dateutil" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" },
+ { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" },
+ { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" },
+ { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" },
+ { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" },
+ { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" },
+ { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" },
+ { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" },
+ { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" },
+ { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" },
+ { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" },
+ { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" },
+ { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" },
+ { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" },
+ { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" },
+ { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" },
+ { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" },
+]
+
+[[package]]
+name = "mediapipe"
+version = "0.10.35"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "absl-py" },
+ { name = "certifi" },
+ { name = "flatbuffers" },
+ { name = "matplotlib" },
+ { name = "numpy" },
+ { name = "opencv-contrib-python" },
+ { name = "sounddevice" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/aa/c2/439e948d2a9a542498aee5d8fa3fb91ac6f4478be5d502f0c78ca3fb2333/mediapipe-0.10.35-py3-none-macosx_11_0_arm64.whl", hash = "sha256:3b31376f34ca3665e34b834565996464cd66c9c91316e914fa7f149c891ce7ac", size = 17782605, upload-time = "2026-04-27T17:45:29.457Z" },
+ { url = "https://files.pythonhosted.org/packages/32/8f/1bc57dbc9b7b03c8f875aac23380ec57e9002cc02fe6720045fb263f3966/mediapipe-0.10.35-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:db9a579df48cffe9570cd3e93f6a5d2dd089a1103b846c60c5b5de8a21c38db0", size = 12400490, upload-time = "2026-04-27T17:45:31.848Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/f6/763477e9aeed98accc984ed6ee3f11a21a0c5fd1d1c6586b8d07067748ff/mediapipe-0.10.35-py3-none-win_amd64.whl", hash = "sha256:b08f001cf3c3cd0d88d9ed68f3368dc8a4913f568281a93117f083115aa672ba", size = 10905503, upload-time = "2026-04-27T17:45:33.946Z" },
+ { url = "https://files.pythonhosted.org/packages/07/b3/5c7fa594c731e8dafab9f1a46ab6cef670fa62dbbfb6248cc70e42ec6fc5/mediapipe-0.10.35-py3-none-win_arm64.whl", hash = "sha256:46255326a6213118aaa518a7aa25e35f93337e82677960cc2a945f117bff8444", size = 9992331, upload-time = "2026-04-27T17:45:36.193Z" },
+]
+
+[[package]]
+name = "mypy-extensions"
+version = "1.1.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" },
+]
+
+[[package]]
+name = "numpy"
+version = "2.4.6"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d0/ad/fed0499ce6a338d2a03ebae59cd15093910c8875328855781952abf6c2fe/numpy-2.4.6.tar.gz", hash = "sha256:f3a3570c4a2a16746ac2c31a7c7c7b0c186b95ce902e33db6f28094ed7387dda", size = 20735807, upload-time = "2026-05-18T23:37:14.07Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fb/82/bdab26d7438c6791ca31b7c024ca37c1eab8b726ba236129005cd4a06e45/numpy-2.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:511dbaf848decaaaf4b4ca48032619fb3138710c4bf7da7617765edad1ef96b0", size = 16684648, upload-time = "2026-05-18T23:34:29.41Z" },
+ { url = "https://files.pythonhosted.org/packages/1b/30/a80189bcc7f5e4258b3fbc3968d909d1756f54d023299ecc39ad6fdb9ef8/numpy-2.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:bf162abab1c1a736333192707cef898e735a5ca00f38f27eeedf44b39d9e85eb", size = 14693902, upload-time = "2026-05-18T23:34:33.013Z" },
+ { url = "https://files.pythonhosted.org/packages/97/12/70b5d0d7c15e1ebb8a6a84a8caa1d19e181d84fb58bb6d70aca29099dec1/numpy-2.4.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:043191bfa8eab18c776647b62723ac9dddece59743b13f49b2016094129c2b3f", size = 5198992, upload-time = "2026-05-18T23:34:36.132Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8c/ebd2a8f8a83541f8d38cc5667e8c2b69cecfd30da6e45693e8158857d44b/numpy-2.4.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:6180d8b35af935aed8ece3a85e0a43f87393ae0ac87c8d2c8bd2c993f7270ef3", size = 6546944, upload-time = "2026-05-18T23:34:38.484Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/c5/7b863a97a91671a0338f4253bd3b5a3d3852f0692dae91711c9f4a10e787/numpy-2.4.6-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72fbe16c6fac95aedf5937fa873445cec2110be35d8a4e9433d7501fd98dae6b", size = 15669392, upload-time = "2026-05-18T23:34:41.257Z" },
+ { url = "https://files.pythonhosted.org/packages/a5/9d/3584b9984ca4c047aea75214ce1a4c4c73d849bd71b604264b7f5653f8a8/numpy-2.4.6-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a7830bab239b79cda9c08c2da014761cafb48da6150e1da17ac06283f43b6089", size = 16633220, upload-time = "2026-05-18T23:34:45.075Z" },
+ { url = "https://files.pythonhosted.org/packages/05/ae/7c67fba23bd98caec7c99261f3a16072ade14813486b0282cb29846de832/numpy-2.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ef4aea96ce4d3b074422cb4f2f64e216bf9e213004bb58ecfdf50ea02ea8eb9a", size = 17020800, upload-time = "2026-05-18T23:34:49.065Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/5d/3b6725cb31d983c5e66916f5d36f6d7e5521129e4c4404d64f918292a5b6/numpy-2.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dfa20cc6ca228e6b155b11da03825975ce66aea520985dbbddf0f2a5a495c605", size = 18357600, upload-time = "2026-05-18T23:34:52.709Z" },
+ { url = "https://files.pythonhosted.org/packages/f7/da/2ccc6c2fe8898dee01d90c75c5f5f914a23daf99e3e0f59516a08760c8b5/numpy-2.4.6-cp313-cp313-win32.whl", hash = "sha256:56b39e5e0622a09a25bf5baf62f4bcf0cb8a41ae6e2819cf49bbc5a74c083f91", size = 5961134, upload-time = "2026-05-18T23:34:55.618Z" },
+ { url = "https://files.pythonhosted.org/packages/b5/cd/9cc4dc876fb065d5c220aae4d5e14826b2715331bb7618ce1fb07a679d99/numpy-2.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:c4fc99836233ea196540b17ab0983aff60ed07941751930f5f4d05bc3b3b7359", size = 12318598, upload-time = "2026-05-18T23:34:58.928Z" },
+ { url = "https://files.pythonhosted.org/packages/39/1e/c0bcba1f8694116485fe28fd1be698c278fcda4141c5b0e53a2aed8b12a8/numpy-2.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:a7c711e21628b52034bb5ab8d1bce291f752fcc5e92accc615778acee1ff4778", size = 10222272, upload-time = "2026-05-18T23:35:02.167Z" },
+ { url = "https://files.pythonhosted.org/packages/63/6d/cc5619247c8f4204e507f5883528372e4ac4bb189e579fb859a12e480b1f/numpy-2.4.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:112b06a867b235ef466ed3508ddf0238050df9c727cafb5301ac385b899189a1", size = 14821197, upload-time = "2026-05-18T23:35:05.468Z" },
+ { url = "https://files.pythonhosted.org/packages/00/58/f1c39161c87d9e9bed660f1ed4bafc0e403d5ec9650b6dd77aead07d489b/numpy-2.4.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:eaf7fa2de5c0be8ae6ff8e9bea2ccd725e980541244521d8d4b5f3354a27babe", size = 5326287, upload-time = "2026-05-18T23:35:08.693Z" },
+ { url = "https://files.pythonhosted.org/packages/af/57/3917ab0fd97f271a8694513581b8a36c655f111c446852c302f04ccdb6fc/numpy-2.4.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:7265a2f3d436e54ef9f2b52b5c937e6be778781bd97a590319d7348f1c1ca997", size = 6646763, upload-time = "2026-05-18T23:35:11.459Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/0f/037e64c494b67581ae18193d770adef354c41f3f2c8ebf865602d949bf8f/numpy-2.4.6-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f74a575920ab21fe304421a3fc28793d82e299cae9eccb37084e9fc7f3617c20", size = 15728070, upload-time = "2026-05-18T23:35:14.79Z" },
+ { url = "https://files.pythonhosted.org/packages/21/a6/5d2bae9c9542eb4df16dc9c46dc79c186e9bad53805dfa5399a6023c6db0/numpy-2.4.6-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ede83e07a75dd06bc501566c1eca2afc0d61677c1472ac9ad93fdee6e638a48d", size = 16681752, upload-time = "2026-05-18T23:35:18.836Z" },
+ { url = "https://files.pythonhosted.org/packages/92/14/23d1dfb410ae362cd59ce53e936b1513d545eb40db3949ced632e19a459e/numpy-2.4.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:68bb27509ac1b9a3443094260f6326150663b06abe40b73a2f81160623da5b67", size = 17086024, upload-time = "2026-05-18T23:35:22.52Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/6e/23595a2c642cdf3bc567877064bdd7f91c8b0038a4453cf2daf7248eafe9/numpy-2.4.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:a0df0043bdb289bde1f62da130d20df23d58b45429f752bc7a8fc5325a225ecd", size = 18403398, upload-time = "2026-05-18T23:35:26.398Z" },
+ { url = "https://files.pythonhosted.org/packages/8a/90/0ac3bc947217e66dec77e7cbc6a1979d1af70b6461b82f620d3bccd5e4c8/numpy-2.4.6-cp313-cp313t-win32.whl", hash = "sha256:29a287e0cf63ff528da061de6b9f64a4618da591ca1046aafc54062e40ca7eab", size = 6084971, upload-time = "2026-05-18T23:35:29.387Z" },
+ { url = "https://files.pythonhosted.org/packages/77/71/5673e351671a1d2bd6063b91b44f70c0affea7d1516fa7a6572941ba4aa1/numpy-2.4.6-cp313-cp313t-win_amd64.whl", hash = "sha256:25c692919ac5a01f170a3bfcd62d745b24fd095c353d50812637d6fcab442e75", size = 12458532, upload-time = "2026-05-18T23:35:32.175Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/88/19d3503c5046e688f049274b27a3ef3d771152fa80d3ba3d01a3dff61abe/numpy-2.4.6-cp313-cp313t-win_arm64.whl", hash = "sha256:1e978ec1e8bd0e0e4de6bb75de9d30cbb74db6b6a2bb727618613703ca0167dd", size = 10291881, upload-time = "2026-05-18T23:35:35.465Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/91/3ab2044d05fd16d343c5ac2e69b127f1b2854040dd20b193257c78028bd3/numpy-2.4.6-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:06ca2f61ec4385a07a6977c55ba998a4466c123642b4a32694d3128fce18c079", size = 16683458, upload-time = "2026-05-18T23:35:38.353Z" },
+ { url = "https://files.pythonhosted.org/packages/8e/62/764ce66fa4147ae6d73071a3abf804ffe606f174618697c571acdf26a7c9/numpy-2.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:38efbc8de75c7a0fc1ac190162d892787f3f47b57cc291231aafee36b80982b7", size = 14704559, upload-time = "2026-05-18T23:35:42.14Z" },
+ { url = "https://files.pythonhosted.org/packages/60/61/23f27c172f022e04025b7dc2367f4d63c1a398120607ec896228649a6f48/numpy-2.4.6-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:d581b735e177fdcdce6fed8e7e8880a3fb6ee4e3653a3ac6af01c6f4c03effc5", size = 5209716, upload-time = "2026-05-18T23:35:45.377Z" },
+ { url = "https://files.pythonhosted.org/packages/03/71/21cf70dc6ea3e3acb95fc53a265b2fc248b981f0194ceb5b475271b8809d/numpy-2.4.6-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:0a041d3d761dc3c35cc56ce0351506a02bcbc25f7b169f652435141a17db9096", size = 6543947, upload-time = "2026-05-18T23:35:47.926Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/91/64288395ee1799bd2e0b04a305dce9666da90c961e1f3fe982a05ee1c036/numpy-2.4.6-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:40fdc1ae7125e518ea98e53e69a4ebc27e1fd50510c47b7ea130cf21e5e1d42b", size = 15685197, upload-time = "2026-05-18T23:35:50.863Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/eb/ebffaa97dc55502df69584a8f0dcf07f69a3e0b3e2323670a2722db9aa39/numpy-2.4.6-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a2c306dea656c12c68f51f4cea133cbe78ca7435eb28c735eac1d3ebe73be6e8", size = 16638245, upload-time = "2026-05-18T23:35:54.752Z" },
+ { url = "https://files.pythonhosted.org/packages/b8/0b/54f9da33128d7e350fab89c7455902eeae70349ee52bddb448dc4a576f45/numpy-2.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:33111801a01c12a8a1e3721f0a9232f8cfc8ae2c6b7098167e6f623c6073f402", size = 17036587, upload-time = "2026-05-18T23:35:58.355Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/f0/fdebc1052db1cc37c64beb22072d67cd6d1c71adca1299f53dec2b5e20d3/numpy-2.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ae506e6902902557576a26ff33eda8695e7ecb3cb36c3b573a0765dee114ebdb", size = 18363226, upload-time = "2026-05-18T23:36:02.845Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/b4/298628d98c72b57e57f7165ae6a481a1deaf6f3c28262a6e4c739c275930/numpy-2.4.6-cp314-cp314-win32.whl", hash = "sha256:aaf159caa35993cb1f56fb9b8e4610d35758e7ca005412eb1daa856a78c9c4b1", size = 6010196, upload-time = "2026-05-18T23:36:05.92Z" },
+ { url = "https://files.pythonhosted.org/packages/df/ac/46de6dda46478f7942f839e094970be2d4a861e005c4b3bf07c92e291a09/numpy-2.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:b507f5c4c1d508876d1819b6bf9a49d365b96320b5d4993426b33a23ca4b8261", size = 12450334, upload-time = "2026-05-18T23:36:09.107Z" },
+ { url = "https://files.pythonhosted.org/packages/78/92/b8b798ac784102c0da830d2257d59358e3d3d90d1e2b3f2575dad976c5cf/numpy-2.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:6f41ae150c4e32db4f3310cdaf64b1593a03dbabe29eec77fc9b50fe64061df6", size = 10495678, upload-time = "2026-05-18T23:36:12.766Z" },
+ { url = "https://files.pythonhosted.org/packages/30/34/ec28d1aa8115971537c01469ab2011ee96827930f0a124de1000cc2a7ed7/numpy-2.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ece3d2cfe132e7d51f44a832b303895e6f2d499c5e74dfbdb06ee246147a304a", size = 14823672, upload-time = "2026-05-18T23:36:16.473Z" },
+ { url = "https://files.pythonhosted.org/packages/16/bd/f6d1fede4e54e8042a7ff97bb495510f3c220f94bcd9e8b228e87c92cc0d/numpy-2.4.6-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:e3e5193ef5a3dc73bceee50f7fdc2c90dbb76c42df8d8fae3d1067a583df579e", size = 5328731, upload-time = "2026-05-18T23:36:19.767Z" },
+ { url = "https://files.pythonhosted.org/packages/f4/f0/e105b9e2fd728a9910103884decd6951d9dd73896b914a98d9a231de02ee/numpy-2.4.6-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:17f9ade344e7d9b464a084d69bcf18fc691cb1db67c62ed80820bf4926d78f0e", size = 6649805, upload-time = "2026-05-18T23:36:22.266Z" },
+ { url = "https://files.pythonhosted.org/packages/82/dd/1206a7ca6ab15e3f02069707ca96222e202af681bb73756da7527f3cb837/numpy-2.4.6-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cd5ffd25db4e7ba6a375693b3fc0fc1791ec636c17db3720da19bde7180ec43", size = 15730496, upload-time = "2026-05-18T23:36:25.713Z" },
+ { url = "https://files.pythonhosted.org/packages/51/e7/38d3ea825dcab85a591734decb2f6c67caa7c8367d374df1a1c3842f9b07/numpy-2.4.6-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7d92c3819208a60205a12a245c91ad70cb0a85336659b19b834205573ac8456e", size = 16679616, upload-time = "2026-05-18T23:36:29.652Z" },
+ { url = "https://files.pythonhosted.org/packages/93/b7/caabfdf53edf663e0b4eb74d7d405d83baef09eb5e83bcd32d601d72b93e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e85b752a1e912b70eaad4fafbd4d1238007ab221de2009b9a2f5ae7461239895", size = 17085145, upload-time = "2026-05-18T23:36:33.449Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/45/68d7c33a6bcf3e5aa3bdbd57a367e6f615286dfd6482f97e8ffeb734306e/numpy-2.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:29cb7f67d10b479ff07c17d33e39f78c07f71c40ef30d63c153d340e96cd3fb4", size = 18403813, upload-time = "2026-05-18T23:36:37.369Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/50/0753655aa844c99cd9e018aacf76f130f1bd81d881bb74bc0aef5d73a8ba/numpy-2.4.6-cp314-cp314t-win32.whl", hash = "sha256:260a5d70215b61ab4fadf5c7baacd64821842975eea312125ed3c39a6391b063", size = 6156982, upload-time = "2026-05-18T23:36:40.817Z" },
+ { url = "https://files.pythonhosted.org/packages/b2/d4/7c67becf668f973cb490cec3e98dfd799d866f9c989a54d355672cfa0db6/numpy-2.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:81a1cca95ed5bb92aa8b10dd2cdc9a0d3853a50fad926c28b5d7e8ea54389627", size = 12638908, upload-time = "2026-05-18T23:36:43.996Z" },
+ { url = "https://files.pythonhosted.org/packages/43/bb/e1c71a4295b1b1d1393d50dbb4f2a36283c6859d9d3892e84f00ec5a91d5/numpy-2.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:0c9136e14ed34a9e343a31c533d78a9813a69a3148332bce5e9821cb2f996e66", size = 10565867, upload-time = "2026-05-18T23:36:47.114Z" },
+]
+
+[[package]]
+name = "opencv-contrib-python"
+version = "4.13.0.92"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/7e/4c/a45c96b9fe90b2c48ee604f5176eb7deb46ce7c2e87c8d819d2945dbcab6/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:53c8ab81376210dda5836307eb6bda7266f39a3820a9a070c7131510ba815fe1", size = 52041546, upload-time = "2026-02-05T07:01:29.918Z" },
+ { url = "https://files.pythonhosted.org/packages/8b/6c/ba1f3177927deeb3002b62fb8db89daea3b5dc732d61de5bf4c73ed6ebf7/opencv_contrib_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:1973d0fc773873f9d1b5bf0d1b65895da2f47b06ba033b7d58393f5c28ba0778", size = 38830319, upload-time = "2026-02-05T07:01:47.222Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/7a/fe87eaf109b454af4a2579f46958b3cafb0f804b9c788c108760723a9bb7/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5f9cb522dd9e465dfca3536c15288f7936b9827432fb9c885eaf94dc5f88c2a3", size = 53339457, upload-time = "2026-02-05T10:09:02.332Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/27/3665ca4b75ddfd218f9ab139f0463d9571e87aaf59391d3c4f5546c08df7/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f9ceec5886419860a31b518991a99e978e5a6a78dca1470103ad4ede0155f156", size = 76591184, upload-time = "2026-02-05T10:11:51.298Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/11/10c46e9527c4591d5264117debd8fe0e21bb23dbf378ce760add6b1e85b6/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:a3c54377c5cf9c45d9b1a207df26dc8fe4f1042d07036cb17d80930c04b25d97", size = 52544155, upload-time = "2026-02-05T10:13:32.068Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/f6/3c645c21358079097201090de7c30d110f5ec3fa01008e3ee81b0a77a354/opencv_contrib_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:fc5ee50e2be9d40e913536f7f20cc6f87f25d8e413ebb32a3335ab6edf245d3e", size = 79150872, upload-time = "2026-02-05T10:16:03.465Z" },
+ { url = "https://files.pythonhosted.org/packages/90/d7/bf4622e0ed8a93f5a685c76933e287477cf185a160c66478cf144fece489/opencv_contrib_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:f5d02357f4d5575c300eab3ec1c7ecfed3a9a53e55a76927bab7cfc9e0a67b68", size = 36829959, upload-time = "2026-02-05T07:02:22.25Z" },
+ { url = "https://files.pythonhosted.org/packages/d9/98/a03f69ff6fb86a67d584ecc990d85a95e6930b96e3f39ad1f8e019cb8ada/opencv_contrib_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:cb694dcf76bb2c8d7fa573fc1a99339e8b6640194d7778381e74cc3445369e45", size = 46486178, upload-time = "2026-02-05T07:02:19.551Z" },
+]
+
+[[package]]
+name = "opencv-python"
+version = "4.13.0.92"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "numpy" },
+]
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/fc/6f/5a28fef4c4a382be06afe3938c64cc168223016fa520c5abaf37e8862aa5/opencv_python-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:caf60c071ec391ba51ed00a4a920f996d0b64e3e46068aac1f646b5de0326a19", size = 46247052, upload-time = "2026-02-05T07:01:25.046Z" },
+ { url = "https://files.pythonhosted.org/packages/08/ac/6c98c44c650b8114a0fb901691351cfb3956d502e8e9b5cd27f4ee7fbf2f/opencv_python-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:5868a8c028a0b37561579bfb8ac1875babdc69546d236249fff296a8c010ccf9", size = 32568781, upload-time = "2026-02-05T07:01:41.379Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/51/82fed528b45173bf629fa44effb76dff8bc9f4eeaee759038362dfa60237/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bc2596e68f972ca452d80f444bc404e08807d021fbba40df26b61b18e01838a", size = 47685527, upload-time = "2026-02-05T06:59:11.24Z" },
+ { url = "https://files.pythonhosted.org/packages/db/07/90b34a8e2cf9c50fe8ed25cac9011cde0676b4d9d9c973751ac7616223a2/opencv_python-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:402033cddf9d294693094de5ef532339f14ce821da3ad7df7c9f6e8316da32cf", size = 70460872, upload-time = "2026-02-05T06:59:19.162Z" },
+ { url = "https://files.pythonhosted.org/packages/02/6d/7a9cc719b3eaf4377b9c2e3edeb7ed3a81de41f96421510c0a169ca3cfd4/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:bccaabf9eb7f897ca61880ce2869dcd9b25b72129c28478e7f2a5e8dee945616", size = 46708208, upload-time = "2026-02-05T06:59:15.419Z" },
+ { url = "https://files.pythonhosted.org/packages/fd/55/b3b49a1b97aabcfbbd6c7326df9cb0b6fa0c0aefa8e89d500939e04aa229/opencv_python-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:620d602b8f7d8b8dab5f4b99c6eb353e78d3fb8b0f53db1bd258bb1aa001c1d5", size = 72927042, upload-time = "2026-02-05T06:59:23.389Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/17/de5458312bcb07ddf434d7bfcb24bb52c59635ad58c6e7c751b48949b009/opencv_python-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:372fe164a3148ac1ca51e5f3ad0541a4a276452273f503441d718fab9c5e5f59", size = 30932638, upload-time = "2026-02-05T07:02:14.98Z" },
+ { url = "https://files.pythonhosted.org/packages/e9/a5/1be1516390333ff9be3a9cb648c9f33df79d5096e5884b5df71a588af463/opencv_python-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:423d934c9fafb91aad38edf26efb46da91ffbc05f3f59c4b0c72e699720706f5", size = 40212062, upload-time = "2026-02-05T07:02:12.724Z" },
+]
+
+[[package]]
+name = "packaging"
+version = "26.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
+]
+
+[[package]]
+name = "pathspec"
+version = "1.1.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/5a/82/42f767fc1c1143d6fd36efb827202a2d997a375e160a71eb2888a925aac1/pathspec-1.1.1.tar.gz", hash = "sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a", size = 135180, upload-time = "2026-04-27T01:46:08.907Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/f1/d9/7fb5aa316bc299258e68c73ba3bddbc499654a07f151cba08f6153988714/pathspec-1.1.1-py3-none-any.whl", hash = "sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189", size = 57328, upload-time = "2026-04-27T01:46:07.06Z" },
+]
+
+[[package]]
+name = "pillow"
+version = "12.2.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/8c/21/c2bcdd5906101a30244eaffc1b6e6ce71a31bd0742a01eb89e660ebfac2d/pillow-12.2.0.tar.gz", hash = "sha256:a830b1a40919539d07806aa58e1b114df53ddd43213d9c8b75847eee6c0182b5", size = 46987819, upload-time = "2026-04-01T14:46:17.687Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/4a/01/53d10cf0dbad820a8db274d259a37ba50b88b24768ddccec07355382d5ad/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:8297651f5b5679c19968abefd6bb84d95fe30ef712eb1b2d9b2d31ca61267f4c", size = 4100837, upload-time = "2026-04-01T14:43:41.506Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/98/f3a6657ecb698c937f6c76ee564882945f29b79bad496abcba0e84659ec5/pillow-12.2.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:50d8520da2a6ce0af445fa6d648c4273c3eeefbc32d7ce049f22e8b5c3daecc2", size = 4176528, upload-time = "2026-04-01T14:43:43.773Z" },
+ { url = "https://files.pythonhosted.org/packages/69/bc/8986948f05e3ea490b8442ea1c1d4d990b24a7e43d8a51b2c7d8b1dced36/pillow-12.2.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:766cef22385fa1091258ad7e6216792b156dc16d8d3fa607e7545b2b72061f1c", size = 3640401, upload-time = "2026-04-01T14:43:45.87Z" },
+ { url = "https://files.pythonhosted.org/packages/34/46/6c717baadcd62bc8ed51d238d521ab651eaa74838291bda1f86fe1f864c9/pillow-12.2.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5d2fd0fa6b5d9d1de415060363433f28da8b1526c1c129020435e186794b3795", size = 5308094, upload-time = "2026-04-01T14:43:48.438Z" },
+ { url = "https://files.pythonhosted.org/packages/71/43/905a14a8b17fdb1ccb58d282454490662d2cb89a6bfec26af6d3520da5ec/pillow-12.2.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:56b25336f502b6ed02e889f4ece894a72612fe885889a6e8c4c80239ff6e5f5f", size = 4695402, upload-time = "2026-04-01T14:43:51.292Z" },
+ { url = "https://files.pythonhosted.org/packages/73/dd/42107efcb777b16fa0393317eac58f5b5cf30e8392e266e76e51cff28c3d/pillow-12.2.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f1c943e96e85df3d3478f7b691f229887e143f81fedab9b20205349ab04d73ed", size = 6280005, upload-time = "2026-04-01T14:43:54.242Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/68/b93e09e5e8549019e61acf49f65b1a8530765a7f812c77a7461bca7e4494/pillow-12.2.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:03f6fab9219220f041c74aeaa2939ff0062bd5c364ba9ce037197f4c6d498cd9", size = 8090669, upload-time = "2026-04-01T14:43:57.335Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/6e/3ccb54ce8ec4ddd1accd2d89004308b7b0b21c4ac3d20fa70af4760a4330/pillow-12.2.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cdfebd752ec52bf5bb4e35d9c64b40826bc5b40a13df7c3cda20a2c03a0f5ed", size = 6395194, upload-time = "2026-04-01T14:43:59.864Z" },
+ { url = "https://files.pythonhosted.org/packages/67/ee/21d4e8536afd1a328f01b359b4d3997b291ffd35a237c877b331c1c3b71c/pillow-12.2.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eedf4b74eda2b5a4b2b2fb4c006d6295df3bf29e459e198c90ea48e130dc75c3", size = 7082423, upload-time = "2026-04-01T14:44:02.74Z" },
+ { url = "https://files.pythonhosted.org/packages/78/5f/e9f86ab0146464e8c133fe85df987ed9e77e08b29d8d35f9f9f4d6f917ba/pillow-12.2.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:00a2865911330191c0b818c59103b58a5e697cae67042366970a6b6f1b20b7f9", size = 6505667, upload-time = "2026-04-01T14:44:05.381Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/1e/409007f56a2fdce61584fd3acbc2bbc259857d555196cedcadc68c015c82/pillow-12.2.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1e1757442ed87f4912397c6d35a0db6a7b52592156014706f17658ff58bbf795", size = 7208580, upload-time = "2026-04-01T14:44:08.39Z" },
+ { url = "https://files.pythonhosted.org/packages/23/c4/7349421080b12fb35414607b8871e9534546c128a11965fd4a7002ccfbee/pillow-12.2.0-cp313-cp313-win32.whl", hash = "sha256:144748b3af2d1b358d41286056d0003f47cb339b8c43a9ea42f5fea4d8c66b6e", size = 6375896, upload-time = "2026-04-01T14:44:11.197Z" },
+ { url = "https://files.pythonhosted.org/packages/3f/82/8a3739a5e470b3c6cbb1d21d315800d8e16bff503d1f16b03a4ec3212786/pillow-12.2.0-cp313-cp313-win_amd64.whl", hash = "sha256:390ede346628ccc626e5730107cde16c42d3836b89662a115a921f28440e6a3b", size = 7081266, upload-time = "2026-04-01T14:44:13.947Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/25/f968f618a062574294592f668218f8af564830ccebdd1fa6200f598e65c5/pillow-12.2.0-cp313-cp313-win_arm64.whl", hash = "sha256:8023abc91fba39036dbce14a7d6535632f99c0b857807cbbbf21ecc9f4717f06", size = 2463508, upload-time = "2026-04-01T14:44:16.312Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/a4/b342930964e3cb4dce5038ae34b0eab4653334995336cd486c5a8c25a00c/pillow-12.2.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:042db20a421b9bafecc4b84a8b6e444686bd9d836c7fd24542db3e7df7baad9b", size = 5309927, upload-time = "2026-04-01T14:44:18.89Z" },
+ { url = "https://files.pythonhosted.org/packages/9f/de/23198e0a65a9cf06123f5435a5d95cea62a635697f8f03d134d3f3a96151/pillow-12.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:dd025009355c926a84a612fecf58bb315a3f6814b17ead51a8e48d3823d9087f", size = 4698624, upload-time = "2026-04-01T14:44:21.115Z" },
+ { url = "https://files.pythonhosted.org/packages/01/a6/1265e977f17d93ea37aa28aa81bad4fa597933879fac2520d24e021c8da3/pillow-12.2.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:88ddbc66737e277852913bd1e07c150cc7bb124539f94c4e2df5344494e0a612", size = 6321252, upload-time = "2026-04-01T14:44:23.663Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/83/5982eb4a285967baa70340320be9f88e57665a387e3a53a7f0db8231a0cd/pillow-12.2.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d362d1878f00c142b7e1a16e6e5e780f02be8195123f164edf7eddd911eefe7c", size = 8126550, upload-time = "2026-04-01T14:44:26.772Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/48/6ffc514adce69f6050d0753b1a18fd920fce8cac87620d5a31231b04bfc5/pillow-12.2.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2c727a6d53cb0018aadd8018c2b938376af27914a68a492f59dfcaca650d5eea", size = 6433114, upload-time = "2026-04-01T14:44:29.615Z" },
+ { url = "https://files.pythonhosted.org/packages/36/a3/f9a77144231fb8d40ee27107b4463e205fa4677e2ca2548e14da5cf18dce/pillow-12.2.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:efd8c21c98c5cc60653bcb311bef2ce0401642b7ce9d09e03a7da87c878289d4", size = 7115667, upload-time = "2026-04-01T14:44:32.773Z" },
+ { url = "https://files.pythonhosted.org/packages/c1/fc/ac4ee3041e7d5a565e1c4fd72a113f03b6394cc72ab7089d27608f8aaccb/pillow-12.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9f08483a632889536b8139663db60f6724bfcb443c96f1b18855860d7d5c0fd4", size = 6538966, upload-time = "2026-04-01T14:44:35.252Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/a8/27fb307055087f3668f6d0a8ccb636e7431d56ed0750e07a60547b1e083e/pillow-12.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:dac8d77255a37e81a2efcbd1fc05f1c15ee82200e6c240d7e127e25e365c39ea", size = 7238241, upload-time = "2026-04-01T14:44:37.875Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/4b/926ab182c07fccae9fcb120043464e1ff1564775ec8864f21a0ebce6ac25/pillow-12.2.0-cp313-cp313t-win32.whl", hash = "sha256:ee3120ae9dff32f121610bb08e4313be87e03efeadfc6c0d18f89127e24d0c24", size = 6379592, upload-time = "2026-04-01T14:44:40.336Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/c4/f9e476451a098181b30050cc4c9a3556b64c02cf6497ea421ac047e89e4b/pillow-12.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:325ca0528c6788d2a6c3d40e3568639398137346c3d6e66bb61db96b96511c98", size = 7085542, upload-time = "2026-04-01T14:44:43.251Z" },
+ { url = "https://files.pythonhosted.org/packages/00/a4/285f12aeacbe2d6dc36c407dfbbe9e96d4a80b0fb710a337f6d2ad978c75/pillow-12.2.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2e5a76d03a6c6dcef67edabda7a52494afa4035021a79c8558e14af25313d453", size = 2465765, upload-time = "2026-04-01T14:44:45.996Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/98/4595daa2365416a86cb0d495248a393dfc84e96d62ad080c8546256cb9c0/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:3adc9215e8be0448ed6e814966ecf3d9952f0ea40eb14e89a102b87f450660d8", size = 4100848, upload-time = "2026-04-01T14:44:48.48Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/79/40184d464cf89f6663e18dfcf7ca21aae2491fff1a16127681bf1fa9b8cf/pillow-12.2.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:6a9adfc6d24b10f89588096364cc726174118c62130c817c2837c60cf08a392b", size = 4176515, upload-time = "2026-04-01T14:44:51.353Z" },
+ { url = "https://files.pythonhosted.org/packages/b0/63/703f86fd4c422a9cf722833670f4f71418fb116b2853ff7da722ea43f184/pillow-12.2.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:6a6e67ea2e6feda684ed370f9a1c52e7a243631c025ba42149a2cc5934dec295", size = 3640159, upload-time = "2026-04-01T14:44:53.588Z" },
+ { url = "https://files.pythonhosted.org/packages/71/e0/fb22f797187d0be2270f83500aab851536101b254bfa1eae10795709d283/pillow-12.2.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2bb4a8d594eacdfc59d9e5ad972aa8afdd48d584ffd5f13a937a664c3e7db0ed", size = 5312185, upload-time = "2026-04-01T14:44:56.039Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/8c/1a9e46228571de18f8e28f16fabdfc20212a5d019f3e3303452b3f0a580d/pillow-12.2.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:80b2da48193b2f33ed0c32c38140f9d3186583ce7d516526d462645fd98660ae", size = 4695386, upload-time = "2026-04-01T14:44:58.663Z" },
+ { url = "https://files.pythonhosted.org/packages/70/62/98f6b7f0c88b9addd0e87c217ded307b36be024d4ff8869a812b241d1345/pillow-12.2.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:22db17c68434de69d8ecfc2fe821569195c0c373b25cccb9cbdacf2c6e53c601", size = 6280384, upload-time = "2026-04-01T14:45:01.5Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/03/688747d2e91cfbe0e64f316cd2e8005698f76ada3130d0194664174fa5de/pillow-12.2.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7b14cc0106cd9aecda615dd6903840a058b4700fcb817687d0ee4fc8b6e389be", size = 8091599, upload-time = "2026-04-01T14:45:04.5Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/35/577e22b936fcdd66537329b33af0b4ccfefaeabd8aec04b266528cddb33c/pillow-12.2.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cbeb542b2ebc6fcdacabf8aca8c1a97c9b3ad3927d46b8723f9d4f033288a0f", size = 6396021, upload-time = "2026-04-01T14:45:07.117Z" },
+ { url = "https://files.pythonhosted.org/packages/11/8d/d2532ad2a603ca2b93ad9f5135732124e57811d0168155852f37fbce2458/pillow-12.2.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4bfd07bc812fbd20395212969e41931001fd59eb55a60658b0e5710872e95286", size = 7083360, upload-time = "2026-04-01T14:45:09.763Z" },
+ { url = "https://files.pythonhosted.org/packages/5e/26/d325f9f56c7e039034897e7380e9cc202b1e368bfd04d4cbe6a441f02885/pillow-12.2.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:9aba9a17b623ef750a4d11b742cbafffeb48a869821252b30ee21b5e91392c50", size = 6507628, upload-time = "2026-04-01T14:45:12.378Z" },
+ { url = "https://files.pythonhosted.org/packages/5f/f7/769d5632ffb0988f1c5e7660b3e731e30f7f8ec4318e94d0a5d674eb65a4/pillow-12.2.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:deede7c263feb25dba4e82ea23058a235dcc2fe1f6021025dc71f2b618e26104", size = 7209321, upload-time = "2026-04-01T14:45:15.122Z" },
+ { url = "https://files.pythonhosted.org/packages/6a/7a/c253e3c645cd47f1aceea6a8bacdba9991bf45bb7dfe927f7c893e89c93c/pillow-12.2.0-cp314-cp314-win32.whl", hash = "sha256:632ff19b2778e43162304d50da0181ce24ac5bb8180122cbe1bf4673428328c7", size = 6479723, upload-time = "2026-04-01T14:45:17.797Z" },
+ { url = "https://files.pythonhosted.org/packages/cd/8b/601e6566b957ca50e28725cb6c355c59c2c8609751efbecd980db44e0349/pillow-12.2.0-cp314-cp314-win_amd64.whl", hash = "sha256:4e6c62e9d237e9b65fac06857d511e90d8461a32adcc1b9065ea0c0fa3a28150", size = 7217400, upload-time = "2026-04-01T14:45:20.529Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/94/220e46c73065c3e2951bb91c11a1fb636c8c9ad427ac3ce7d7f3359b9b2f/pillow-12.2.0-cp314-cp314-win_arm64.whl", hash = "sha256:b1c1fbd8a5a1af3412a0810d060a78b5136ec0836c8a4ef9aa11807f2a22f4e1", size = 2554835, upload-time = "2026-04-01T14:45:23.162Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/ab/1b426a3974cb0e7da5c29ccff4807871d48110933a57207b5a676cccc155/pillow-12.2.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:57850958fe9c751670e49b2cecf6294acc99e562531f4bd317fa5ddee2068463", size = 5314225, upload-time = "2026-04-01T14:45:25.637Z" },
+ { url = "https://files.pythonhosted.org/packages/19/1e/dce46f371be2438eecfee2a1960ee2a243bbe5e961890146d2dee1ff0f12/pillow-12.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d5d38f1411c0ed9f97bcb49b7bd59b6b7c314e0e27420e34d99d844b9ce3b6f3", size = 4698541, upload-time = "2026-04-01T14:45:28.355Z" },
+ { url = "https://files.pythonhosted.org/packages/55/c3/7fbecf70adb3a0c33b77a300dc52e424dc22ad8cdc06557a2e49523b703d/pillow-12.2.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c0a9f29ca8e79f09de89293f82fc9b0270bb4af1d58bc98f540cc4aedf03166", size = 6322251, upload-time = "2026-04-01T14:45:30.924Z" },
+ { url = "https://files.pythonhosted.org/packages/1c/3c/7fbc17cfb7e4fe0ef1642e0abc17fc6c94c9f7a16be41498e12e2ba60408/pillow-12.2.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1610dd6c61621ae1cf811bef44d77e149ce3f7b95afe66a4512f8c59f25d9ebe", size = 8127807, upload-time = "2026-04-01T14:45:33.908Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/c3/a8ae14d6defd2e448493ff512fae903b1e9bd40b72efb6ec55ce0048c8ce/pillow-12.2.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a34329707af4f73cf1782a36cd2289c0368880654a2c11f027bcee9052d35dd", size = 6433935, upload-time = "2026-04-01T14:45:36.623Z" },
+ { url = "https://files.pythonhosted.org/packages/6e/32/2880fb3a074847ac159d8f902cb43278a61e85f681661e7419e6596803ed/pillow-12.2.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8e9c4f5b3c546fa3458a29ab22646c1c6c787ea8f5ef51300e5a60300736905e", size = 7116720, upload-time = "2026-04-01T14:45:39.258Z" },
+ { url = "https://files.pythonhosted.org/packages/46/87/495cc9c30e0129501643f24d320076f4cc54f718341df18cc70ec94c44e1/pillow-12.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:fb043ee2f06b41473269765c2feae53fc2e2fbf96e5e22ca94fb5ad677856f06", size = 6540498, upload-time = "2026-04-01T14:45:41.879Z" },
+ { url = "https://files.pythonhosted.org/packages/18/53/773f5edca692009d883a72211b60fdaf8871cbef075eaa9d577f0a2f989e/pillow-12.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:f278f034eb75b4e8a13a54a876cc4a5ab39173d2cdd93a638e1b467fc545ac43", size = 7239413, upload-time = "2026-04-01T14:45:44.705Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/e4/4b64a97d71b2a83158134abbb2f5bd3f8a2ea691361282f010998f339ec7/pillow-12.2.0-cp314-cp314t-win32.whl", hash = "sha256:6bb77b2dcb06b20f9f4b4a8454caa581cd4dd0643a08bacf821216a16d9c8354", size = 6482084, upload-time = "2026-04-01T14:45:47.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ba/13/306d275efd3a3453f72114b7431c877d10b1154014c1ebbedd067770d629/pillow-12.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6562ace0d3fb5f20ed7290f1f929cae41b25ae29528f2af1722966a0a02e2aa1", size = 7225152, upload-time = "2026-04-01T14:45:50.032Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/6e/cf826fae916b8658848d7b9f38d88da6396895c676e8086fc0988073aaf8/pillow-12.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:aa88ccfe4e32d362816319ed727a004423aab09c5cea43c01a4b435643fa34eb", size = 2556579, upload-time = "2026-04-01T14:45:52.529Z" },
+]
+
+[[package]]
+name = "platformdirs"
+version = "4.10.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/d7/47/e4501f49c178ae1d9f4a75073fda4204f52647993f075a9db4d14930e0c5/platformdirs-4.10.0.tar.gz", hash = "sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7", size = 31224, upload-time = "2026-05-28T03:32:53.587Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/81/e6/cd9575ac904136b3cbf7aa7ee819ef86eedb7274e46f230e94ea4342e729/platformdirs-4.10.0-py3-none-any.whl", hash = "sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a", size = 22743, upload-time = "2026-05-28T03:32:52.175Z" },
+]
+
+[[package]]
+name = "pycparser"
+version = "3.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/1b/7d/92392ff7815c21062bea51aa7b87d45576f649f16458d78b7cf94b9ab2e6/pycparser-3.0.tar.gz", hash = "sha256:600f49d217304a5902ac3c37e1281c9fe94e4d0489de643a9504c5cdfdfc6b29", size = 103492, upload-time = "2026-01-21T14:26:51.89Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/0c/c3/44f3fbbfa403ea2a7c779186dc20772604442dde72947e7d01069cbe98e3/pycparser-3.0-py3-none-any.whl", hash = "sha256:b727414169a36b7d524c1c3e31839a521725078d7b2ff038656844266160a992", size = 48172, upload-time = "2026-01-21T14:26:50.693Z" },
+]
+
+[[package]]
+name = "pyparsing"
+version = "3.3.2"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" },
+]
+
+[[package]]
+name = "python-dateutil"
+version = "2.9.0.post0"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "six" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" },
+]
+
+[[package]]
+name = "pytokens"
+version = "0.4.1"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/b6/34/b4e015b99031667a7b960f888889c5bd34ef585c85e1cb56a594b92836ac/pytokens-0.4.1.tar.gz", hash = "sha256:292052fe80923aae2260c073f822ceba21f3872ced9a68bb7953b348e561179a", size = 23015, upload-time = "2026-01-30T01:03:45.924Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/cb/dc/08b1a080372afda3cceb4f3c0a7ba2bde9d6a5241f1edb02a22a019ee147/pytokens-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8bdb9d0ce90cbf99c525e75a2fa415144fd570a1ba987380190e8b786bc6ef9b", size = 160720, upload-time = "2026-01-30T01:03:13.843Z" },
+ { url = "https://files.pythonhosted.org/packages/64/0c/41ea22205da480837a700e395507e6a24425151dfb7ead73343d6e2d7ffe/pytokens-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5502408cab1cb18e128570f8d598981c68a50d0cbd7c61312a90507cd3a1276f", size = 254204, upload-time = "2026-01-30T01:03:14.886Z" },
+ { url = "https://files.pythonhosted.org/packages/e0/d2/afe5c7f8607018beb99971489dbb846508f1b8f351fcefc225fcf4b2adc0/pytokens-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29d1d8fb1030af4d231789959f21821ab6325e463f0503a61d204343c9b355d1", size = 268423, upload-time = "2026-01-30T01:03:15.936Z" },
+ { url = "https://files.pythonhosted.org/packages/68/d4/00ffdbd370410c04e9591da9220a68dc1693ef7499173eb3e30d06e05ed1/pytokens-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:970b08dd6b86058b6dc07efe9e98414f5102974716232d10f32ff39701e841c4", size = 266859, upload-time = "2026-01-30T01:03:17.458Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/c9/c3161313b4ca0c601eeefabd3d3b576edaa9afdefd32da97210700e47652/pytokens-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:9bd7d7f544d362576be74f9d5901a22f317efc20046efe2034dced238cbbfe78", size = 103520, upload-time = "2026-01-30T01:03:18.652Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/a7/b470f672e6fc5fee0a01d9e75005a0e617e162381974213a945fcd274843/pytokens-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4a14d5f5fc78ce85e426aa159489e2d5961acf0e47575e08f35584009178e321", size = 160821, upload-time = "2026-01-30T01:03:19.684Z" },
+ { url = "https://files.pythonhosted.org/packages/80/98/e83a36fe8d170c911f864bfded690d2542bfcfacb9c649d11a9e6eb9dc41/pytokens-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97f50fd18543be72da51dd505e2ed20d2228c74e0464e4262e4899797803d7fa", size = 254263, upload-time = "2026-01-30T01:03:20.834Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/95/70d7041273890f9f97a24234c00b746e8da86df462620194cef1d411ddeb/pytokens-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dc74c035f9bfca0255c1af77ddd2d6ae8419012805453e4b0e7513e17904545d", size = 268071, upload-time = "2026-01-30T01:03:21.888Z" },
+ { url = "https://files.pythonhosted.org/packages/da/79/76e6d09ae19c99404656d7db9c35dfd20f2086f3eb6ecb496b5b31163bad/pytokens-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f66a6bbe741bd431f6d741e617e0f39ec7257ca1f89089593479347cc4d13324", size = 271716, upload-time = "2026-01-30T01:03:23.633Z" },
+ { url = "https://files.pythonhosted.org/packages/79/37/482e55fa1602e0a7ff012661d8c946bafdc05e480ea5a32f4f7e336d4aa9/pytokens-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:b35d7e5ad269804f6697727702da3c517bb8a5228afa450ab0fa787732055fc9", size = 104539, upload-time = "2026-01-30T01:03:24.788Z" },
+ { url = "https://files.pythonhosted.org/packages/30/e8/20e7db907c23f3d63b0be3b8a4fd1927f6da2395f5bcc7f72242bb963dfe/pytokens-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8fcb9ba3709ff77e77f1c7022ff11d13553f3c30299a9fe246a166903e9091eb", size = 168474, upload-time = "2026-01-30T01:03:26.428Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/81/88a95ee9fafdd8f5f3452107748fd04c24930d500b9aba9738f3ade642cc/pytokens-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:79fc6b8699564e1f9b521582c35435f1bd32dd06822322ec44afdeba666d8cb3", size = 290473, upload-time = "2026-01-30T01:03:27.415Z" },
+ { url = "https://files.pythonhosted.org/packages/cf/35/3aa899645e29b6375b4aed9f8d21df219e7c958c4c186b465e42ee0a06bf/pytokens-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d31b97b3de0f61571a124a00ffe9a81fb9939146c122c11060725bd5aea79975", size = 303485, upload-time = "2026-01-30T01:03:28.558Z" },
+ { url = "https://files.pythonhosted.org/packages/52/a0/07907b6ff512674d9b201859f7d212298c44933633c946703a20c25e9d81/pytokens-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:967cf6e3fd4adf7de8fc73cd3043754ae79c36475c1c11d514fc72cf5490094a", size = 306698, upload-time = "2026-01-30T01:03:29.653Z" },
+ { url = "https://files.pythonhosted.org/packages/39/2a/cbbf9250020a4a8dd53ba83a46c097b69e5eb49dd14e708f496f548c6612/pytokens-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:584c80c24b078eec1e227079d56dc22ff755e0ba8654d8383b2c549107528918", size = 116287, upload-time = "2026-01-30T01:03:30.912Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/78/397db326746f0a342855b81216ae1f0a32965deccfd7c830a2dbc66d2483/pytokens-0.4.1-py3-none-any.whl", hash = "sha256:26cef14744a8385f35d0e095dc8b3a7583f6c953c2e3d269c7f82484bf5ad2de", size = 13729, upload-time = "2026-01-30T01:03:45.029Z" },
+]
+
+[[package]]
+name = "six"
+version = "1.17.0"
+source = { registry = "https://pypi.org/simple" }
+sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" },
+]
+
+[[package]]
+name = "sounddevice"
+version = "0.5.5"
+source = { registry = "https://pypi.org/simple" }
+dependencies = [
+ { name = "cffi" },
+]
+sdist = { url = "https://files.pythonhosted.org/packages/2a/f9/2592608737553638fca98e21e54bfec40bf577bb98a61b2770c912aab25e/sounddevice-0.5.5.tar.gz", hash = "sha256:22487b65198cb5bf2208755105b524f78ad173e5ab6b445bdab1c989f6698df3", size = 143191, upload-time = "2026-01-23T18:36:43.529Z" }
+wheels = [
+ { url = "https://files.pythonhosted.org/packages/1e/0a/478e441fd049002cf308520c0d62dd8333e7c6cc8d997f0dda07b9fbcc46/sounddevice-0.5.5-py3-none-any.whl", hash = "sha256:30ff99f6c107f49d25ad16a45cacd8d91c25a1bcdd3e81a206b921a3a6405b1f", size = 32807, upload-time = "2026-01-23T18:36:35.649Z" },
+ { url = "https://files.pythonhosted.org/packages/56/f9/c037c35f6d0b6bc3bc7bfb314f1d6f1f9a341328ef47cd63fc4f850a7b27/sounddevice-0.5.5-py3-none-macosx_10_6_x86_64.macosx_10_6_universal2.whl", hash = "sha256:05eb9fd6c54c38d67741441c19164c0dae8ce80453af2d8c4ad2e7823d15b722", size = 108557, upload-time = "2026-01-23T18:36:37.41Z" },
+ { url = "https://files.pythonhosted.org/packages/88/a1/d19dd9889cd4bce2e233c4fac007cd8daaf5b9fe6e6a5d432cf17be0b807/sounddevice-0.5.5-py3-none-win32.whl", hash = "sha256:1234cc9b4c9df97b6cbe748146ae0ec64dd7d6e44739e8e42eaa5b595313a103", size = 317765, upload-time = "2026-01-23T18:36:39.047Z" },
+ { url = "https://files.pythonhosted.org/packages/c3/0e/002ed7c4c1c2ab69031f78989d3b789fee3a7fba9e586eb2b81688bf4961/sounddevice-0.5.5-py3-none-win_amd64.whl", hash = "sha256:cfc6b2c49fb7f555591c78cb8ecf48d6a637fd5b6e1db5fec6ed9365d64b3519", size = 365324, upload-time = "2026-01-23T18:36:40.496Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/39/a61d4b83a7746b70d23d9173be688c0c6bfc7173772344b7442c2c155497/sounddevice-0.5.5-py3-none-win_arm64.whl", hash = "sha256:3861901ddd8230d2e0e8ae62ac320cdd4c688d81df89da036dcb812f757bb3e6", size = 317115, upload-time = "2026-01-23T18:36:42.235Z" },
+]
diff --git a/web/License b/web/License
new file mode 100644
index 0000000..1ed8d37
--- /dev/null
+++ b/web/License
@@ -0,0 +1,18 @@
+EyeGesturesLite License
+
+Copyright (c) 2024 Piotr Walas
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"),
+to use the Software in personal and commercial projects, subject to the following conditions:
+
+Attribution:
+The EyeGesturesLite logo must be prominently displayed in all projects or products that use this Software.
+
+No Warranty:
+The Software is provided "as is", without warranty of any kind, express or implied,
+including but not limited to the warranties of merchantability,
+fitness for a particular purpose, and noninfringement.
+In no event shall the authors or copyright holders be liable for any claim,
+damages, or other liability, whether in an action of contract, tort,
+or otherwise, arising from, out of, or in connection with
+the Software or the use or other dealings in the Software.
diff --git a/web/Makefile b/web/Makefile
new file mode 100644
index 0000000..6fae293
--- /dev/null
+++ b/web/Makefile
@@ -0,0 +1,17 @@
+.PHONY: all clean min wasm
+
+MINIFY = npx terser
+MINIFY_FLAGS = --compress --mangle
+
+SRC = src/eyegestures.js
+MIN = $(SRC:.js=.min.js)
+
+min:
+ $(MINIFY) $(SRC) $(MINIFY_FLAGS) -o engine/pkg/eyegestures.min.js
+
+wasm:
+ cd ./engine && wasm-pack build --target web
+ $(MINIFY) $(SRC) $(MINIFY_FLAGS) -o engine/pkg/eyegestures.min.js
+
+clean:
+ rm -f src/eyegestures.min.js
\ No newline at end of file
diff --git a/web/README.md b/web/README.md
new file mode 100644
index 0000000..4f641de
--- /dev/null
+++ b/web/README.md
@@ -0,0 +1,140 @@
+## EYEGESTURES
+
+
+
+
+
+
+
+
+
+
+
+
+EyeGestures is open source eyetracking software/library using native webcams and phone camers for achieving its goal. The aim of library is to bring accessibility of eye-tracking and eye-driven interfaces without requirement of obtaining expensive hardware.
+
+Our [Mission](https://github.com/NativeSensors/EyeGestures/blob/main/MISSION.md)!
+
+# EyeGesturesLite
+
+EyeGesturesLite is JavaScript implementation of [EyeGestures algoritm](https://github.com/NativeSensors/EyeGestures). If you need python version, check original repository.
+
+### How does it work?
+
+It is a gaze tracker that uses machine learning and built-in cameras (such as a webcam) to provide gaze tracking for the user. It includes a built-in calibration mode that displays 20 red circles for the user to focus on, along with a blue cursor that follows the user’s gaze. During the calibration process, the cursor will gradually start following user's gaze more and more. By the end of the 20 points, the cursor should be able to independently follow the user’s gaze.
+
+### ⚙️ Try:
+
+[EyeGesturesLite](https://eyegestures.com/tryLite)
+
+### 🔧 Build your own:
+
+#### CDN:
+
+0. External dependencies CDNs:
+
+```html
+
+
+```
+
+1. You need two CDN links:
+```html
+
+
+```
+
+2. Place `video` element (which can be hidden) somewhere in the page together with status and error divs (can stay invisible):
+```html
+
+Initializing...
+
+```
+
+3. Then javascript interface code:
+```html
+
+```
+
+#### NPM package [WiP]:
+
+1. Install npm package:
+```
+npm install eyegestures
+```
+
+2. Place `video` element in your DOM (which can be hidden) somewhere in the page together with status and error divs (can stay invisible):
+```html
+
+Initializing...
+
+```
+
+3. try to import:
+```JS
+import EyeGestures from 'eyegestures';
+
+const gestures = new EyeGestures("video_element_id",(point,calibration)=>{/*point,calibration*/})
+// after this call: gestures.start();
+console.log(gestures);
+```
+>[!Warning]
+> EyeGestures needs DOM to operate and its constructor expects to receive video element/camera feed id string.
+>
+
+### rules of using
+
+You can use it free of charge as long as you keep our logo. If you want to remove logo then contact: contact@eyegestures.com.
+
+### 📇 Find us:
+- [discord](https://discord.gg/FvagCX8T4h)
+- [twitter](https://twitter.com/PW4ltz)
+- [daily.dev](https://dly.to/JEe1Sz6vLey)
+- email: contact@eyegestures.com
+
+### Troubleshooting:
+
+
+### 💻 Contributors
+
+
+
+
+
+### 💵 Support the project
+
+We will be extremely grateful for your support: it helps to keep server running + fuels our brains with coffee.
+
+Support project on Polar (in exchange we provide access to alphas versions!):
+
+
+
+
+
+
+
+
diff --git a/web/demo/eyekey.html b/web/demo/eyekey.html
new file mode 100644
index 0000000..851edc1
--- /dev/null
+++ b/web/demo/eyekey.html
@@ -0,0 +1,355 @@
+
+
+
+
+
+ Eye QWERTY Keyboard · EyeGestures
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Complete calibration to unlock keyboard
+
+
+
+
+
+ Dwell time:
+
+ 1.5 s
+
+ Look at a key and hold gaze to select
+ x: — y: —
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/example/EyeGesturesDark.png b/web/example/EyeGesturesDark.png
new file mode 100644
index 0000000..e47ecc5
Binary files /dev/null and b/web/example/EyeGesturesDark.png differ
diff --git a/web/example/EyeGesturesLight.png b/web/example/EyeGesturesLight.png
new file mode 100644
index 0000000..3efbd32
Binary files /dev/null and b/web/example/EyeGesturesLight.png differ
diff --git a/web/example/eyegestures.css b/web/example/eyegestures.css
new file mode 100644
index 0000000..146f2e3
--- /dev/null
+++ b/web/example/eyegestures.css
@@ -0,0 +1,26 @@
+#cursor {
+ position: fixed;
+ width: 50px;
+ height: 50px;
+ background: rgba(0, 0, 255, 0.342);
+ border-radius: 50%;
+ border: medium solid rgba(0, 0, 255, 0.171);
+ z-index: 1000;
+}
+#calib_cursor {
+ position: fixed;
+ width: 200px;
+ height: 200px;
+ background: #ff575777;
+ border-radius: 50%;
+ border: medium solid #ff5757;
+ z-index: 10000;
+}
+#logoDivEyeGestures{
+}
+#logoDivEyeGestures:hover{
+ transform: scale(1.01);
+}
+#logoDivEyeGestures:active{
+ transform: scale(1.0);
+}
\ No newline at end of file
diff --git a/web/example/simple.html b/web/example/simple.html
new file mode 100644
index 0000000..85f1f17
--- /dev/null
+++ b/web/example/simple.html
@@ -0,0 +1,90 @@
+
+
+
+
+
+ EyeGesturesLite
+
+
+
+
+
+
+
+
+
EyeGesturesLite
+
Initializing...
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/web/eyegestures.js b/web/eyegestures.js
new file mode 100644
index 0000000..8558d4f
--- /dev/null
+++ b/web/eyegestures.js
@@ -0,0 +1,2 @@
+import EyeGestures from './src/eyegestures.min.js';
+export default EyeGestures
\ No newline at end of file
diff --git a/web/npm_test.js b/web/npm_test.js
new file mode 100644
index 0000000..afcddac
--- /dev/null
+++ b/web/npm_test.js
@@ -0,0 +1,5 @@
+import EyeGestures from 'eyegestures';
+
+const gestures = new EyeGestures("video_element_id",(point,calibration)=>{/*point,calibration*/})
+// after this call: gestures.start();
+console.log(gestures);
diff --git a/web/package-lock.json b/web/package-lock.json
new file mode 100644
index 0000000..abdf653
--- /dev/null
+++ b/web/package-lock.json
@@ -0,0 +1,141 @@
+{
+ "name": "eyegestures",
+ "version": "1.0.0",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "eyegestures",
+ "version": "1.0.0",
+ "license": "SEE LICENSE IN License",
+ "dependencies": {
+ "eyegestures": "^1.0.0"
+ },
+ "devDependencies": {
+ "terser": "^5.48.0"
+ }
+ },
+ "node_modules/@jridgewell/gen-mapping": {
+ "version": "0.3.13",
+ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz",
+ "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/sourcemap-codec": "^1.5.0",
+ "@jridgewell/trace-mapping": "^0.3.24"
+ }
+ },
+ "node_modules/@jridgewell/resolve-uri": {
+ "version": "3.1.2",
+ "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz",
+ "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==",
+ "dev": true,
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.0.0"
+ }
+ },
+ "node_modules/@jridgewell/source-map": {
+ "version": "0.3.11",
+ "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz",
+ "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/gen-mapping": "^0.3.5",
+ "@jridgewell/trace-mapping": "^0.3.25"
+ }
+ },
+ "node_modules/@jridgewell/sourcemap-codec": {
+ "version": "1.5.5",
+ "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz",
+ "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/@jridgewell/trace-mapping": {
+ "version": "0.3.31",
+ "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz",
+ "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@jridgewell/resolve-uri": "^3.1.0",
+ "@jridgewell/sourcemap-codec": "^1.4.14"
+ }
+ },
+ "node_modules/acorn": {
+ "version": "8.16.0",
+ "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz",
+ "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==",
+ "dev": true,
+ "license": "MIT",
+ "bin": {
+ "acorn": "bin/acorn"
+ },
+ "engines": {
+ "node": ">=0.4.0"
+ }
+ },
+ "node_modules/buffer-from": {
+ "version": "1.1.2",
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/commander": {
+ "version": "2.20.3",
+ "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz",
+ "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
+ "dev": true,
+ "license": "MIT"
+ },
+ "node_modules/eyegestures": {
+ "version": "1.0.0",
+ "resolved": "https://registry.npmjs.org/eyegestures/-/eyegestures-1.0.0.tgz",
+ "integrity": "sha512-tb+q3/TJtUMkKPUdUjx+22uGYyXnQwQiJWAG+84NlLTmnGuo3lKWOfVowphqkJ4eXrUg0u0LhtB93Ri347tFyw=="
+ },
+ "node_modules/source-map": {
+ "version": "0.6.1",
+ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
+ "dev": true,
+ "license": "BSD-3-Clause",
+ "engines": {
+ "node": ">=0.10.0"
+ }
+ },
+ "node_modules/source-map-support": {
+ "version": "0.5.21",
+ "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
+ "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "buffer-from": "^1.0.0",
+ "source-map": "^0.6.0"
+ }
+ },
+ "node_modules/terser": {
+ "version": "5.48.0",
+ "resolved": "https://registry.npmjs.org/terser/-/terser-5.48.0.tgz",
+ "integrity": "sha512-J/9An6vs9Us6wKRriSFXBWdRZapREHqFzdNUKk0pmu804EMR6dr6winwo7e5JDxN4xahxQsuysyYFwlwj4XN/Q==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "dependencies": {
+ "@jridgewell/source-map": "^0.3.3",
+ "acorn": "^8.15.0",
+ "commander": "^2.20.0",
+ "source-map-support": "~0.5.20"
+ },
+ "bin": {
+ "terser": "bin/terser"
+ },
+ "engines": {
+ "node": ">=10"
+ }
+ }
+ }
+}
diff --git a/web/package.json b/web/package.json
new file mode 100644
index 0000000..7a0d4a2
--- /dev/null
+++ b/web/package.json
@@ -0,0 +1,38 @@
+{
+ "name": "eyegestures",
+ "version": "1.0.0",
+ "type": "module",
+ "description": "Gaze tracking algorithm for web.",
+ "main": "eyegestures.js",
+ "directories": {
+ "src": "src"
+ },
+ "scripts": {
+ "test": "echo \"Error: no test specified\" && exit 1"
+ },
+ "repository": {
+ "type": "git",
+ "url": "git+https://github.com/NativeSensors/EyeGesturesLite.git"
+ },
+ "keywords": [
+ "eye",
+ "tracking",
+ "gaze",
+ "tracking",
+ "eyegestures",
+ "eyetracking",
+ "gazetracking"
+ ],
+ "author": "Piotr Walas",
+ "license": "SEE LICENSE IN License",
+ "bugs": {
+ "url": "https://github.com/NativeSensors/EyeGesturesLite/issues"
+ },
+ "homepage": "https://github.com/NativeSensors/EyeGesturesLite#readme",
+ "dependencies": {
+ "eyegestures": "^1.0.0"
+ },
+ "devDependencies": {
+ "terser": "^5.48.0"
+ }
+}
diff --git a/web/src/eyegestures.js b/web/src/eyegestures.js
new file mode 100644
index 0000000..de9a295
--- /dev/null
+++ b/web/src/eyegestures.js
@@ -0,0 +1,455 @@
+const __eyeGesturesScriptUrl = (document.currentScript && document.currentScript.src) || window.location.href;
+let __eyeGesturesEngineModulePromise = null;
+
+function __resolveEngineModuleUrl() {
+ if (window.EyeGesturesEngineModuleUrl) {
+ return window.EyeGesturesEngineModuleUrl;
+ }
+ console.log("__eyeGesturesScriptUrl: ", __eyeGesturesScriptUrl);
+ return new URL('EyegesturesEngine.js', __eyeGesturesScriptUrl).href;
+}
+
+function __loadEngineModule() {
+ if (!__eyeGesturesEngineModulePromise) {
+ __eyeGesturesEngineModulePromise = import(__resolveEngineModuleUrl());
+ }
+ return __eyeGesturesEngineModulePromise;
+}
+
+VERSION = "4.0.0";
+class EyeGestures{ // strip export default before making cdn/web embeddable version
+ constructor(videoElement_ID, onGaze)
+ {
+ const cursor = document.createElement('div');
+ cursor.id = "cursor";
+ cursor.style.display = "None";
+ document.body.appendChild(cursor);
+ const calib_cursor = document.createElement('div');
+ calib_cursor.id = "calib_cursor";
+ calib_cursor.style.display = "None";
+
+ const logoDiv = document.createElement('div');
+ logoDiv.id = "logoDivEyeGestures";
+ logoDiv.style.width = "200px";
+ logoDiv.style.height = "60px";
+ logoDiv.style.position = "fixed";
+ logoDiv.style.bottom = "10px";
+ logoDiv.style.right = "10px";
+ logoDiv.style.zIndex = "9999";
+ logoDiv.style.background = "black";
+ logoDiv.style.borderRadius = "10px";
+ logoDiv.style.display = "none";
+ logoDiv.onclick = function() {
+ window.location.href = "https://eyegestures.com/";
+ };
+ const logo = document.createElement('div');
+ logo.style.margin = "10px";
+ logo.innerHTML = ' ';
+ logoDiv.appendChild(logo);
+ const canvas = document.createElement('canvas');
+ canvas.id = "output_canvas";
+ canvas.width = "50";
+ canvas.height = "50";
+ canvas.style.margin = "5px";
+ canvas.style.borderRadius = "10px";
+ canvas.style.border = "none";
+ canvas.style.background = "#222";
+ logoDiv.appendChild(canvas);
+ document.body.appendChild(logoDiv);
+
+ document.body.appendChild(calib_cursor);
+
+ this.engine = null;
+ this.onGaze = onGaze;
+ this.run = false;
+ this.__invisible = false;
+ this.screen_width = 0;
+ this.screen_height = 0;
+
+ this.updateViewportSize();
+
+ if (window.isSecureContext) {
+ this.init(videoElement_ID);
+ }
+ else {
+ console.error('This application requires a secure context (HTTPS or localhost)');
+ }
+
+ }
+
+ updateViewportSize() {
+ this.screen_width = Math.max(document.documentElement.clientWidth || 0, window.innerWidth || 0);
+ this.screen_height = Math.max(document.documentElement.clientHeight || 0, window.innerHeight || 0);
+ }
+
+ showUpCalibrationInstructions(onRead) {
+ // Create content container
+ // const content = document.createElement('div');
+ // content.style.textAlign = 'center';
+ // content.style.color = '#fff';
+ // content.style.fontFamily = 'Arial, sans-serif';
+
+ // // Create instructional text
+ // const instructionText1 = document.createElement('h3');
+ // instructionText1.textContent = 'EyeGestures Upcalibration:';
+ // instructionText1.style.fontSize = '1.5rem';
+ // instructionText1.style.marginBottom = '20px';
+
+ // const instructionText2 = document.createElement('p');
+ // instructionText2.innerHTML = 'We need to improve calibration. Focus on 25 red circles .';
+ // instructionText2.style.marginBottom = '20px';
+
+ // const instructionText3 = document.createElement('p');
+ // instructionText3.innerHTML = 'The blue circle is your estimated gaze. With every calibration point, the tracker will gradually listen more and more to your gaze.';
+ // instructionText3.style.marginBottom = '20px';
+
+ // // Append elements to content
+ // content.appendChild(instructionText1);
+ // content.appendChild(instructionText2);
+ // content.appendChild(instructionText3);
+ // content.appendChild(button);
+ }
+
+ showCalibrationInstructions(onRead) {
+ // Create overlay
+ const overlay = document.createElement('div');
+ overlay.id = 'calibrationOverlay';
+ overlay.style.position = 'fixed';
+ overlay.style.top = '0';
+ overlay.style.left = '0';
+ overlay.style.width = '100vw';
+ overlay.style.height = '100vh';
+ overlay.style.backgroundColor = 'rgba(0, 0, 0, 0.9)';
+ overlay.style.display = 'flex';
+ overlay.style.justifyContent = 'center';
+ overlay.style.alignItems = 'center';
+ overlay.style.zIndex = '1000';
+
+ // Create content container
+ const content = document.createElement('div');
+ content.style.textAlign = 'center';
+ content.style.color = '#fff';
+ content.style.fontFamily = 'Arial, sans-serif';
+
+ // Create instructional text
+ const instructionText1 = document.createElement('h3');
+ instructionText1.textContent = 'EyeGestures Calibration:';
+ instructionText1.style.fontSize = '1.5rem';
+ instructionText1.style.marginBottom = '20px';
+
+ const instructionText2 = document.createElement('p');
+ instructionText2.innerHTML = 'To calibrate properly you need to gaze on 25 red circles .';
+ instructionText2.style.marginBottom = '20px';
+
+ const instructionText3 = document.createElement('p');
+ instructionText3.innerHTML = 'The blue circle is your estimated gaze. With every calibration point, the tracker will gradually listen more and more to your gaze.';
+ instructionText3.style.marginBottom = '20px';
+
+ // Create button
+ const button = document.createElement('button');
+ button.textContent = 'Continue';
+ button.style.padding = '10px 20px';
+ button.style.fontSize = '1rem';
+ button.style.border = 'none';
+ button.style.borderRadius = '5px';
+ button.style.backgroundColor = '#5e17eb';
+ button.style.color = '#fff';
+ button.style.cursor = 'pointer';
+
+ // Button click event to remove overlay
+ button.addEventListener('click', () => {
+ document.body.removeChild(overlay);
+ onRead();
+ });
+ // Append elements to content
+ content.appendChild(instructionText1);
+ content.appendChild(instructionText2);
+ content.appendChild(instructionText3);
+ content.appendChild(button);
+
+ // Append content to overlay
+ overlay.appendChild(content);
+
+ // Append overlay to body
+ document.body.appendChild(overlay);
+
+ setTimeout(() => {
+ document.body.removeChild(overlay);
+ onRead();
+ },15000)
+ }
+
+
+ // Status update function
+ updateStatus(message) {
+ document.getElementById('status').textContent = message;
+ }
+
+ // Error display function
+ showError(message) {
+ const errorElement = document.getElementById('error');
+ errorElement.textContent = message;
+ errorElement.style.display = 'block';
+ }
+
+ // Function to load script with promise
+ loadScript(url) {
+ return new Promise((resolve, reject) => {
+ const script = document.createElement('script');
+ script.src = url;
+ script.onload = resolve;
+ script.onerror = reject;
+ document.head.appendChild(script);
+ });
+ }
+
+ // Main initialization
+ async init(videoElement_id) {
+ try {
+ this.updateViewportSize();
+ this.updateStatus('Loading EyeGestures engine...');
+ const engineModule = await __loadEngineModule();
+ await engineModule.default();
+ this.engine = new engineModule.EyeGesturesEngineWasm(this.screen_width, this.screen_height);
+
+ this.updateStatus('Loading MediaPipe library...');
+
+ // Load MediaPipe library
+ await this.loadScript('https://cdn.jsdelivr.net/npm/@mediapipe/drawing_utils@0.3/drawing_utils.js');
+ await this.loadScript('https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633559619/face_mesh.js');
+
+ this.updateStatus('MediaPipe library loaded, initializing...');
+
+ if (typeof FaceMesh === 'undefined') {
+ throw new Error('FaceMesh is not defined. Library not loaded correctly.');
+ }
+
+ await this.setupMediaPipe(videoElement_id);
+ } catch (error) {
+ console.error('Initialization error:', error);
+ this.showError('Initialization error: ' + error.message);
+ }
+ }
+
+ async setupMediaPipe(videoElement_id) {
+ try {
+ // Initialize FaceMesh solution
+ const faceMesh = new FaceMesh({
+ locateFile: (file) => {
+ console.log("Loading file:", file);
+ return `https://cdn.jsdelivr.net/npm/@mediapipe/face_mesh@0.4.1633559619/${file}`;
+ }
+ });
+
+ // Set options for FaceMesh
+ faceMesh.setOptions({
+ maxNumFaces: 1,
+ refineLandmarks: true,
+ minDetectionConfidence: 0.5,
+ minTrackingConfidence: 0.5
+ });
+
+ // Initialize the FaceMesh instance
+ await faceMesh.initialize();
+ this.updateStatus('FaceMesh initialized successfully');
+
+ // Set callback for the results
+ faceMesh.onResults(this.onFaceMeshResults.bind(this));
+
+ // Access video stream
+ const stream = await navigator.mediaDevices.getUserMedia({
+ video: {}
+ });
+
+ const videoElement = document.getElementById(videoElement_id);
+ videoElement.srcObject = stream;
+
+ // Wait for video to be loaded
+ videoElement.onloadeddata = () => {
+ this.updateStatus('Video stream started');
+ videoElement.play();
+ requestAnimationFrame(processFrame);
+ };
+
+ async function processFrame() {
+ const videoElement = document.getElementById("video");
+ if (videoElement.readyState !== videoElement.HAVE_ENOUGH_DATA) {
+ requestAnimationFrame(processFrame);
+ return;
+ }
+
+ const canvas = document.getElementById("output_canvas");
+ const ctx = canvas.getContext("2d");
+ try {
+ // ctx.drawImage(videoElement, 0, 0, canvas.width, canvas.height);
+ ctx.save();
+ ctx.scale(-1, 1); // Flip horizontally (invert x-axis)
+ ctx.translate(-canvas.width, 0); // Adjust translation to ensure the image is drawn correctly
+ faceMesh.send({ image: videoElement });
+ ctx.restore();
+ } catch (error) {
+ console.error('Error processing frame:', error);
+ showError('Error processing frame: ' + error.message);
+ }
+ requestAnimationFrame(processFrame);
+ }
+
+ } catch (error) {
+ console.error('Error initializing MediaPipe:', error);
+ showError('Error initializing MediaPipe: ' + error.message);
+ }
+ }
+
+ onFaceMeshResults(results) {
+ if ((!results.multiFaceLandmarks || !results.multiFaceLandmarks.length) || !this.run) {
+ return;
+ }
+
+ if (!this.engine) {
+ return;
+ }
+
+ this.updateViewportSize();
+
+ // Define eye keypoints before using them
+ const LEFT_EYE_KEYPOINTS = [
+ 33, 133, 160, 159, 158, 157, 173, 155, 154, 153, 144, 145, 153, 246, 468
+ ];
+ const RIGHT_EYE_KEYPOINTS = [
+ 362, 263, 387, 386, 385, 384, 398, 382, 381, 380, 374, 373, 374, 466, 473
+ ];
+
+ let landmarks = results.multiFaceLandmarks[0];
+ const flattened = new Float64Array(landmarks.flatMap(point => [point.x, point.y]));
+ const output = this.engine.process(flattened);
+ const x = output[0];
+ const y = output[1];
+ const calibrating = output[2] === 1;
+ const x_calib = output[3];
+ const y_calib = output[4];
+
+ // visual layer
+ let l_landmarks = LEFT_EYE_KEYPOINTS.map(index => landmarks[index]);
+ let r_landmarks = RIGHT_EYE_KEYPOINTS.map(index => landmarks[index]);
+ let scale_x = 1;
+ let scale_y = 1;
+ let offset_x = 0;
+ let offset_y = 0;
+ let width = x - offset_x;
+ let height = y - offset_y;
+ const canvas = document.getElementById("output_canvas");
+ const ctx = canvas.getContext("2d");
+ ctx.clearRect(0, 0, canvas.width, canvas.height);
+ let left_eye_coordinates = [];
+ let right_eye_coordinates = [];
+
+ ctx.fillStyle = '#ff5757';
+ l_landmarks.forEach(landmark => {
+ left_eye_coordinates.push(
+ [
+ (((landmark.x- offset_x)/width) * scale_x ),
+ (((landmark.y- offset_y)/height) * scale_y )
+ ]
+ );
+ ctx.beginPath();
+ ctx.arc(
+ landmark.x * canvas.width,
+ landmark.y * canvas.height,
+ 3, // radius
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+ });
+
+ // Draw dots for each landmark
+ ctx.fillStyle = '#5e17eb';
+ r_landmarks.forEach(landmark => {
+ right_eye_coordinates.push(
+ [
+ (((landmark.x- offset_x)/width) * scale_x ),
+ (((landmark.y- offset_y)/height) * scale_y )
+ ]
+ );
+ ctx.beginPath();
+ ctx.arc(
+ landmark.x * canvas.width,
+ landmark.y * canvas.height,
+ 3, // radius
+ 0,
+ 2 * Math.PI
+ );
+ ctx.fill();
+ });
+
+ let cursor = document.getElementById("cursor");
+ let left = Math.min(Math.max(x,0),this.screen_width)
+ let top = Math.min(Math.max(y,0),this.screen_height)
+ cursor.style.left = `${left - 25}px`;
+ cursor.style.top = `${top - 25}px`;
+ let calib_cursor = document.getElementById("calib_cursor");
+ let calib_left = Math.min(Math.max(x_calib,0),this.screen_width);
+ let calib_top = Math.min(Math.max(y_calib,0),this.screen_height);
+ calib_cursor.style.left = `${calib_left - 100}px`;
+ calib_cursor.style.top = `${calib_top - 100}px`;
+
+ if(!calibrating){
+ calib_cursor.style.display = "none";
+ }else{
+ calib_cursor.style.display = "block";
+ }
+
+ this.onGaze([x, y], calibrating);
+ }
+
+ __run(){
+ this.run = true;
+ }
+
+ start(){
+ const logoDivEyeGestures = document.getElementById("logoDivEyeGestures");
+ logoDivEyeGestures.style.display = "flex";
+
+ this.showCalibrationInstructions(this.__run.bind(this));
+
+ if(!this.__invisible){
+ let cursor = document.getElementById("cursor");
+ cursor.style.display = "block";
+ }
+
+ let calib_cursor = document.getElementById("calib_cursor");
+ calib_cursor.style.display = "block";
+ // this.run = true;
+ };
+
+ invisible()
+ {
+ this.__invisible = true;
+ let cursor = document.getElementById("cursor");
+ cursor.style.display = "none";
+ }
+
+ visible()
+ {
+ this.__invisible = false;
+ let cursor = document.getElementById("cursor");
+ cursor.style.display = "block";
+ }
+
+ stop(){
+ this.run = false;
+ console.log("stop");
+ };
+
+ recalibrate(){
+ if (this.engine) {
+ this.engine.recalibrate();
+ }
+ };
+
+ addPointToCalibration(x, y){
+ if (this.engine) {
+ this.engine.add_calibration_point(x, y);
+ }
+ };
+}
diff --git a/web/test.js b/web/test.js
new file mode 100644
index 0000000..0b0f40c
--- /dev/null
+++ b/web/test.js
@@ -0,0 +1,3 @@
+import EyeGestures from './eyegestures.js';
+
+console.log(EyeGestures);
\ No newline at end of file