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: -

- Subscribe on Polar - --------- -

- -

- -### ⚓ 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 +

+ + + + + +

+ + + made in PL + + +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 + + + +``` + +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 + + + +``` + +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!): + +Subscribe on Polar + + + + + Star History Chart + 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 + + + + + + + + + + +
+
+ +
+
+ + +
+
+ Starting… +
+
+ +
+
Output
+
+
+
+ +
+
+
+

Complete calibration to unlock keyboard

+
+
+ +
+ + 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 = 'Logo'; + 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