diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000000000000000000000000000000000000..21991110409d245cd05e6f318b31680408fc59be --- /dev/null +++ b/.dockerignore @@ -0,0 +1,12 @@ +# Ignore local data and virtual environments +data/ +.venv/ + +# Common extras you probably don't want in the image +__pycache__/ +*.pyc +*.pyo +*.pyd +.env +.git +.gitignore \ No newline at end of file diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..51d35c214c765557930b50db9f179e468a3df5b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,170 @@ +external/ +tests/data/ +data/datasets/ +.ipynb_checkpoints/ + +# 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 + +# PyInstaller +# Usually these files are written by a python script from a template +# before PyInstaller builds the exe, so as to inject date/other infos into it. +*.manifest +*.spec + +# 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 + +# 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/#use-with-ide +.pdm.toml + +# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm +__pypackages__/ + +# Celery stuff +celerybeat-schedule +celerybeat.pid + +# SageMath parsed files +*.sage.py + +# 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/ + +.gradio/ +best_model.pth + +.DS_Store diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000000000000000000000000000000000..8a7aa0a2e00d7530e872272b4a48de1c4fc3670d --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.4.4 + hooks: + - id: ruff # linter + types_or: [python, pyi, jupyter] + args: [--exit-non-zero-on-fix] + - id: ruff-format # formatter + types_or: [python, pyi, jupyter] \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 0000000000000000000000000000000000000000..69abe370519a5cb89648d5310bb2a27181260a5e --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,15 @@ +{ + "[python]": { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + "source.organizeImports": "explicit" + }, + "editor.defaultFormatter": "charliermarsh.ruff" + }, + "notebook.formatOnSave.enabled": true, + "notebook.codeActionsOnSave": { + "notebook.source.fixAll": "explicit", + "notebook.source.organizeImports": "explicit" + }, +} \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000000000000000000000000000000000..b19961994919b8e979ed1cb0635b8acb790b3584 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,58 @@ +# Documentation: https://huggingface.co/docs/hub/spaces-sdks-docker + +# Start from an official lightweight Python image +FROM python:3.10-slim + +# Prevents Python from writing .pyc files and buffering stdout/stderr +# ENV PYTHONDONTWRITEBYTECODE=1 +# ENV PYTHONUNBUFFERED=1 + +# Install system dependencies +RUN apt-get update && apt-get install -y --no-install-recommends \ + git \ + wget \ + unzip \ + vim-tiny \ + curl \ + libgl1 \ + libglib2.0-0 \ + xournalpp \ + poppler-utils \ + && rm -rf /var/lib/apt/lists/* + +# Create and set working directory +WORKDIR /app + +# Create temp_code_mount folder +RUN mkdir -p /temp_code_mount + +# Install Python dependencies early for caching +# COPY requirements.txt . +# RUN pip install --no-cache-dir -r requirements.txt + +# Copy application code +COPY . . + +# Run the INSTALL_HF_DOCKER_SPACE.sh script +RUN bash INSTALL_HF_DOCKER_SPACE.sh +RUN pip install matplotlib bs4 pdf2image supabase python-dotenv +# ^- that should not be necessary!! TODO!! + +# Expose the port Gradio will run on inside Hugging Face Spaces +EXPOSE 7860 + +# Command to run Gradio app +# Hugging Face Spaces will set PORT env var, so we use it +CMD ["python", "scripts/demo.py"] + + + + + + + + +# https://huggingface.co/docs/hub/spaces-sdks-docker + +# https://huggingface.co/spaces/SpacesExamples/secret-example/tree/main +# - https://huggingface.co/spaces/SpacesExamples/secret-example/blob/main/Dockerfile \ No newline at end of file diff --git a/INSTALL_HF_DOCKER_SPACE.sh b/INSTALL_HF_DOCKER_SPACE.sh new file mode 100644 index 0000000000000000000000000000000000000000..8d746353d7015ce8b3926c324cc1604d870257df --- /dev/null +++ b/INSTALL_HF_DOCKER_SPACE.sh @@ -0,0 +1,52 @@ +# Based on `INSTALL_LINUX.sh` file. + +# ======== +# SETTINGS +# ======== + +HTR_PIPELINE_PATH="external/htr_pipeline" + +# ================ +# Helper functions +# ================ + +install_htr_pipeline () { + + mkdir -p ${HTR_PIPELINE_PATH} + cd ${HTR_PIPELINE_PATH} + git clone https://github.com/githubharald/HTRPipeline.git + cd HTRPipeline + cd htr_pipeline/models + wget https://www.dropbox.com/s/j1hl6bppecug0sz/models.zip + unzip -o models.zip + cd ../../ + pip install . + # 3. Install [HTRPipelines](https://github.com/githubharald/HTRPipeline) package using [its installation guide](https://github.com/githubharald/HTRPipeline/tree/master#installation). + +} + +CURRENT_DIR=$(pwd) + +# ==================== +# Installation process +# ==================== + +rm -rf ${HTR_PIPELINE_PATH} + +install_htr_pipeline +cd ${CURRENT_DIR} +pip install -r requirements.txt +pip install gradio # TODO: Move to optional package in `pyproject.toml` once I use this setup. +pip install -e . + +# ======== +# Feedback +# ======== + +echo +echo "===========================================" +echo "===========================================" +echo "===========================================" +echo +echo "Installation complete" +echo \ No newline at end of file diff --git a/INSTALL_LINUX.sh b/INSTALL_LINUX.sh new file mode 100644 index 0000000000000000000000000000000000000000..28d49c3ab85483032b76d5c4040cc1490958b6c3 --- /dev/null +++ b/INSTALL_LINUX.sh @@ -0,0 +1,62 @@ +# ======== +# SETTINGS +# ======== + +ENVIRONMENT_NAME="xournalpp_htr" +HTR_PIPELINE_PATH="external/htr_pipeline" + +# ================ +# Helper functions +# ================ + +install_htr_pipeline () { + + mkdir -p ${HTR_PIPELINE_PATH} + cd ${HTR_PIPELINE_PATH} + git clone https://github.com/githubharald/HTRPipeline.git + cd HTRPipeline + cd htr_pipeline/models + wget https://www.dropbox.com/s/j1hl6bppecug0sz/models.zip + unzip -o models.zip + cd ../../ + pip install . + # 3. Install [HTRPipelines](https://github.com/githubharald/HTRPipeline) package using [its installation guide](https://github.com/githubharald/HTRPipeline/tree/master#installation). + +} + +CURRENT_DIR=$(pwd) + +# ==================== +# Installation process +# ==================== + +rm -rf ${HTR_PIPELINE_PATH} + +eval "$(conda shell.bash hook)" # enable `conda activate`, see + # https://stackoverflow.com/a/56155771 + +conda create --name ${ENVIRONMENT_NAME} python=3.10.11 -y +conda activate ${ENVIRONMENT_NAME} +install_htr_pipeline +cd ${CURRENT_DIR} +pip install -r requirements.txt +pip install -e . +pre-commit install + +cd plugin +bash copy_to_plugin_folder.sh + +# ======== +# Feedback +# ======== + +echo +echo "===========================================" +echo "===========================================" +echo "===========================================" +echo +echo "Installation complete" +echo +echo "Activate environment with:" +echo "\"conda activate ${ENVIRONMENT_NAME}\"" +echo \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000000000000000000000000000000000000..ecbc0593737be657aef92e3adcf3bcbd6fdb812e --- /dev/null +++ b/LICENSE @@ -0,0 +1,339 @@ + GNU GENERAL PUBLIC LICENSE + Version 2, June 1991 + + Copyright (C) 1989, 1991 Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA + Everyone is permitted to copy and distribute verbatim copies + of this license document, but changing it is not allowed. + + Preamble + + The licenses for most software are designed to take away your +freedom to share and change it. By contrast, the GNU General Public +License is intended to guarantee your freedom to share and change free +software--to make sure the software is free for all its users. This +General Public License applies to most of the Free Software +Foundation's software and to any other program whose authors commit to +using it. (Some other Free Software Foundation software is covered by +the GNU Lesser General Public License instead.) You can apply it to +your programs, too. + + When we speak of free software, we are referring to freedom, not +price. Our General Public Licenses are designed to make sure that you +have the freedom to distribute copies of free software (and charge for +this service if you wish), that you receive source code or can get it +if you want it, that you can change the software or use pieces of it +in new free programs; and that you know you can do these things. + + To protect your rights, we need to make restrictions that forbid +anyone to deny you these rights or to ask you to surrender the rights. +These restrictions translate to certain responsibilities for you if you +distribute copies of the software, or if you modify it. + + For example, if you distribute copies of such a program, whether +gratis or for a fee, you must give the recipients all the rights that +you have. You must make sure that they, too, receive or can get the +source code. And you must show them these terms so they know their +rights. + + We protect your rights with two steps: (1) copyright the software, and +(2) offer you this license which gives you legal permission to copy, +distribute and/or modify the software. + + Also, for each author's protection and ours, we want to make certain +that everyone understands that there is no warranty for this free +software. If the software is modified by someone else and passed on, we +want its recipients to know that what they have is not the original, so +that any problems introduced by others will not reflect on the original +authors' reputations. + + Finally, any free program is threatened constantly by software +patents. We wish to avoid the danger that redistributors of a free +program will individually obtain patent licenses, in effect making the +program proprietary. To prevent this, we have made it clear that any +patent must be licensed for everyone's free use or not licensed at all. + + The precise terms and conditions for copying, distribution and +modification follow. + + GNU GENERAL PUBLIC LICENSE + TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION + + 0. This License applies to any program or other work which contains +a notice placed by the copyright holder saying it may be distributed +under the terms of this General Public License. The "Program", below, +refers to any such program or work, and a "work based on the Program" +means either the Program or any derivative work under copyright law: +that is to say, a work containing the Program or a portion of it, +either verbatim or with modifications and/or translated into another +language. (Hereinafter, translation is included without limitation in +the term "modification".) Each licensee is addressed as "you". + +Activities other than copying, distribution and modification are not +covered by this License; they are outside its scope. The act of +running the Program is not restricted, and the output from the Program +is covered only if its contents constitute a work based on the +Program (independent of having been made by running the Program). +Whether that is true depends on what the Program does. + + 1. You may copy and distribute verbatim copies of the Program's +source code as you receive it, in any medium, provided that you +conspicuously and appropriately publish on each copy an appropriate +copyright notice and disclaimer of warranty; keep intact all the +notices that refer to this License and to the absence of any warranty; +and give any other recipients of the Program a copy of this License +along with the Program. + +You may charge a fee for the physical act of transferring a copy, and +you may at your option offer warranty protection in exchange for a fee. + + 2. You may modify your copy or copies of the Program or any portion +of it, thus forming a work based on the Program, and copy and +distribute such modifications or work under the terms of Section 1 +above, provided that you also meet all of these conditions: + + a) You must cause the modified files to carry prominent notices + stating that you changed the files and the date of any change. + + b) You must cause any work that you distribute or publish, that in + whole or in part contains or is derived from the Program or any + part thereof, to be licensed as a whole at no charge to all third + parties under the terms of this License. + + c) If the modified program normally reads commands interactively + when run, you must cause it, when started running for such + interactive use in the most ordinary way, to print or display an + announcement including an appropriate copyright notice and a + notice that there is no warranty (or else, saying that you provide + a warranty) and that users may redistribute the program under + these conditions, and telling the user how to view a copy of this + License. (Exception: if the Program itself is interactive but + does not normally print such an announcement, your work based on + the Program is not required to print an announcement.) + +These requirements apply to the modified work as a whole. If +identifiable sections of that work are not derived from the Program, +and can be reasonably considered independent and separate works in +themselves, then this License, and its terms, do not apply to those +sections when you distribute them as separate works. But when you +distribute the same sections as part of a whole which is a work based +on the Program, the distribution of the whole must be on the terms of +this License, whose permissions for other licensees extend to the +entire whole, and thus to each and every part regardless of who wrote it. + +Thus, it is not the intent of this section to claim rights or contest +your rights to work written entirely by you; rather, the intent is to +exercise the right to control the distribution of derivative or +collective works based on the Program. + +In addition, mere aggregation of another work not based on the Program +with the Program (or with a work based on the Program) on a volume of +a storage or distribution medium does not bring the other work under +the scope of this License. + + 3. You may copy and distribute the Program (or a work based on it, +under Section 2) in object code or executable form under the terms of +Sections 1 and 2 above provided that you also do one of the following: + + a) Accompany it with the complete corresponding machine-readable + source code, which must be distributed under the terms of Sections + 1 and 2 above on a medium customarily used for software interchange; or, + + b) Accompany it with a written offer, valid for at least three + years, to give any third party, for a charge no more than your + cost of physically performing source distribution, a complete + machine-readable copy of the corresponding source code, to be + distributed under the terms of Sections 1 and 2 above on a medium + customarily used for software interchange; or, + + c) Accompany it with the information you received as to the offer + to distribute corresponding source code. (This alternative is + allowed only for noncommercial distribution and only if you + received the program in object code or executable form with such + an offer, in accord with Subsection b above.) + +The source code for a work means the preferred form of the work for +making modifications to it. For an executable work, complete source +code means all the source code for all modules it contains, plus any +associated interface definition files, plus the scripts used to +control compilation and installation of the executable. However, as a +special exception, the source code distributed need not include +anything that is normally distributed (in either source or binary +form) with the major components (compiler, kernel, and so on) of the +operating system on which the executable runs, unless that component +itself accompanies the executable. + +If distribution of executable or object code is made by offering +access to copy from a designated place, then offering equivalent +access to copy the source code from the same place counts as +distribution of the source code, even though third parties are not +compelled to copy the source along with the object code. + + 4. You may not copy, modify, sublicense, or distribute the Program +except as expressly provided under this License. Any attempt +otherwise to copy, modify, sublicense or distribute the Program is +void, and will automatically terminate your rights under this License. +However, parties who have received copies, or rights, from you under +this License will not have their licenses terminated so long as such +parties remain in full compliance. + + 5. You are not required to accept this License, since you have not +signed it. However, nothing else grants you permission to modify or +distribute the Program or its derivative works. These actions are +prohibited by law if you do not accept this License. Therefore, by +modifying or distributing the Program (or any work based on the +Program), you indicate your acceptance of this License to do so, and +all its terms and conditions for copying, distributing or modifying +the Program or works based on it. + + 6. Each time you redistribute the Program (or any work based on the +Program), the recipient automatically receives a license from the +original licensor to copy, distribute or modify the Program subject to +these terms and conditions. You may not impose any further +restrictions on the recipients' exercise of the rights granted herein. +You are not responsible for enforcing compliance by third parties to +this License. + + 7. If, as a consequence of a court judgment or allegation of patent +infringement or for any other reason (not limited to patent issues), +conditions are imposed on you (whether by court order, agreement or +otherwise) that contradict the conditions of this License, they do not +excuse you from the conditions of this License. If you cannot +distribute so as to satisfy simultaneously your obligations under this +License and any other pertinent obligations, then as a consequence you +may not distribute the Program at all. For example, if a patent +license would not permit royalty-free redistribution of the Program by +all those who receive copies directly or indirectly through you, then +the only way you could satisfy both it and this License would be to +refrain entirely from distribution of the Program. + +If any portion of this section is held invalid or unenforceable under +any particular circumstance, the balance of the section is intended to +apply and the section as a whole is intended to apply in other +circumstances. + +It is not the purpose of this section to induce you to infringe any +patents or other property right claims or to contest validity of any +such claims; this section has the sole purpose of protecting the +integrity of the free software distribution system, which is +implemented by public license practices. Many people have made +generous contributions to the wide range of software distributed +through that system in reliance on consistent application of that +system; it is up to the author/donor to decide if he or she is willing +to distribute software through any other system and a licensee cannot +impose that choice. + +This section is intended to make thoroughly clear what is believed to +be a consequence of the rest of this License. + + 8. If the distribution and/or use of the Program is restricted in +certain countries either by patents or by copyrighted interfaces, the +original copyright holder who places the Program under this License +may add an explicit geographical distribution limitation excluding +those countries, so that distribution is permitted only in or among +countries not thus excluded. In such case, this License incorporates +the limitation as if written in the body of this License. + + 9. The Free Software Foundation may publish revised and/or new versions +of the General Public License from time to time. Such new versions will +be similar in spirit to the present version, but may differ in detail to +address new problems or concerns. + +Each version is given a distinguishing version number. If the Program +specifies a version number of this License which applies to it and "any +later version", you have the option of following the terms and conditions +either of that version or of any later version published by the Free +Software Foundation. If the Program does not specify a version number of +this License, you may choose any version ever published by the Free Software +Foundation. + + 10. If you wish to incorporate parts of the Program into other free +programs whose distribution conditions are different, write to the author +to ask for permission. For software which is copyrighted by the Free +Software Foundation, write to the Free Software Foundation; we sometimes +make exceptions for this. Our decision will be guided by the two goals +of preserving the free status of all derivatives of our free software and +of promoting the sharing and reuse of software generally. + + NO WARRANTY + + 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY +FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN +OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES +PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED +OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS +TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE +PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, +REPAIR OR CORRECTION. + + 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING +WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR +REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, +INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING +OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED +TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY +YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER +PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE +POSSIBILITY OF SUCH DAMAGES. + + END OF TERMS AND CONDITIONS + + How to Apply These Terms to Your New Programs + + If you develop a new program, and you want it to be of the greatest +possible use to the public, the best way to achieve this is to make it +free software which everyone can redistribute and change under these terms. + + To do so, attach the following notices to the program. It is safest +to attach them to the start of each source file to most effectively +convey the exclusion of warranty; and each file should have at least +the "copyright" line and a pointer to where the full notice is found. + + + Copyright (C) + + This program is free software; you can redistribute it and/or modify + it under the terms of the GNU General Public License as published by + the Free Software Foundation; either version 2 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU General Public License for more details. + + You should have received a copy of the GNU General Public License along + with this program; if not, write to the Free Software Foundation, Inc., + 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +Also add information on how to contact you by electronic and paper mail. + +If the program is interactive, make it output a short notice like this +when it starts in an interactive mode: + + Gnomovision version 69, Copyright (C) year name of author + Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. + This is free software, and you are welcome to redistribute it + under certain conditions; type `show c' for details. + +The hypothetical commands `show w' and `show c' should show the appropriate +parts of the General Public License. Of course, the commands you use may +be called something other than `show w' and `show c'; they could even be +mouse-clicks or menu items--whatever suits your program. + +You should also get your employer (if you work as a programmer) or your +school, if any, to sign a "copyright disclaimer" for the program, if +necessary. Here is a sample; alter the names: + + Yoyodyne, Inc., hereby disclaims all copyright interest in the program + `Gnomovision' (which makes passes at compilers) written by James Hacker. + + , 1 April 1989 + Ty Coon, President of Vice + +This General Public License does not permit incorporating your program into +proprietary programs. If your program is a subroutine library, you may +consider it more useful to permit linking proprietary applications with the +library. If this is what you want to do, use the GNU Lesser General +Public License instead of this License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000000000000000000000000000000000..98f461340ebf914ed9ec6e69c7a445a9184ca74d --- /dev/null +++ b/Makefile @@ -0,0 +1,13 @@ +# TODO: Fill it. + +docs: + mkdocs build --clean +# TODO: Sth like https://numpy.org/doc/stable/reference/generated/numpy.mean.html#numpy.mean + +tests-installation: + pytest -v -k "installation" + +run-pre-commit-hooks: + pre-commit run --all-files + +.PHONY: docs tests-installation diff --git a/README.md b/README.md index 98b85354c8b51fd513b6af20c71cfc2ab6ad7949..105c17b06693139cbc34f0b3f0aa6942e55d370e 100644 --- a/README.md +++ b/README.md @@ -1,10 +1,8 @@ --- -title: Xournalpp Htr -emoji: 🚀 -colorFrom: yellow -colorTo: red +title: Xournal++ HTR +emoji: 🐳 +colorFrom: purple +colorTo: gray sdk: docker -pinned: false ---- - -Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference +app_port: 7860 +--- \ No newline at end of file diff --git a/data/.gitkeep b/data/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/data/.gitkeep @@ -0,0 +1 @@ + diff --git a/docs/2024-08-27-22-52_unit_calculations.xoj b/docs/2024-08-27-22-52_unit_calculations.xoj new file mode 100644 index 0000000000000000000000000000000000000000..73304ce691f3a4e0808debaf7b9b2ff72f797966 Binary files /dev/null and b/docs/2024-08-27-22-52_unit_calculations.xoj differ diff --git a/docs/ADRs/2025-10-04_design_of_huggingface_space_dockerfile.md b/docs/ADRs/2025-10-04_design_of_huggingface_space_dockerfile.md new file mode 100644 index 0000000000000000000000000000000000000000..44ebef5f5ec8b43bbb20d40e09c1cd3a87d40fdb --- /dev/null +++ b/docs/ADRs/2025-10-04_design_of_huggingface_space_dockerfile.md @@ -0,0 +1,32 @@ +# Design of HuggingFace Space Dockerfile + +- Status: Ongoing +- Deciders: Martin Lellep (@PellelNitram) +- Drivers: Martin Lellep (@PellelNitram) +- PRD: None +- Date: 2025-10-04 + +## Context + +*Explain the background and the context in which the decision is being made. Include any relevant information about the problem, constraints, or goals.* + +## Decisions + +*State the decision that has been made. Be clear and concise.* + +- In the future, download models at build time into the Docker image from Github release page. In the + very far future, pull them from HuggingFace at run-time. +- Add `xournalpp` binary to Docker image so that the `xopp` file can be exported as PDF prior to + execution of the HTR pipeline. + +## Consequences + +*Describe the consequences of the decision. Include both positive and negative outcomes, as well as any trade-offs.* + +## Alternatives Considered + +*List and briefly describe other options that were considered and why they were not chosen.* + +## References + +*Include links or references to any supporting documentation, discussions, or resources.* \ No newline at end of file diff --git a/docs/annotate_tool_UI_design.svg b/docs/annotate_tool_UI_design.svg new file mode 100644 index 0000000000000000000000000000000000000000..d8b70d6b1919876cc29eaa3b9b22359373b6134b --- /dev/null +++ b/docs/annotate_tool_UI_design.svg @@ -0,0 +1,198 @@ + + + + + + + + + + + + + + + + + + + + listview:- all annotations + annotation:- json - text - bbox coords + page:- first, not zoomable- next, zoomable- click leads to bounding box drawing + + diff --git a/docs/contributing.md b/docs/contributing.md new file mode 100644 index 0000000000000000000000000000000000000000..ddeca75332e51560c78241cf4c0352e98565c2bb --- /dev/null +++ b/docs/contributing.md @@ -0,0 +1,74 @@ +# Contributing + +There are multiple ways to contribute to this project. Below, those ways are explained alongside information on how to best contribute from a codebase point of view. + +Really, we greatly appreciate any help! + +## Ways to contribute + +### Reach out + +If you have questions about how to best contribute or the slightest +interest in contributing, then feel free to reach out to me at any time :-). + +### Issues on Github + +A great way to help out with this project is to check [open issues on Github](https://github.com/PellelNitram/xournalpp_htr/issues) +and to try to work on them. + +If you need support with those, then please reach out to - we're very happy to help! + +## Things to consider when contributing + +### Branching strategy + +The following branching strategy is used to keep the `master` branch stable and +allow for experimentation: `master` > `dev` > `feature branches`. This branching +strategy is shown in the following visualisation and then explained in more detail +in the next paragraph: + +```mermaid +%%{init:{ "gitGraph":{ "mainBranchName":"master" }}}%% +gitGraph + commit + commit + branch dev + commit + checkout dev + commit + commit + branch feature/awesome_new_feature + commit + checkout feature/awesome_new_feature + commit + commit + commit + checkout dev + merge feature/awesome_new_feature + commit + commit + checkout master + merge dev + commit + commit +``` + +In more details, this repository adheres to the following git branching strategy: The +`master` branch remains stable and delivers a functioning product. The `dev` branch +consists of all code that will be merged to `master` eventually where the corresponding +features are developed in individual feature branches; the above visualisation shows an +example feature branch called `feature/awesome_new_feature` that works on a feature +called `awesome_new_feature`. + +Given this structure, please implement new features as feature branches and +rebase them onto the `dev` branch prior to sending a pull request to `dev`. + +Note: The Github Actions CI/CD pipeline runs on the branches `master` and `dev`. + +### Code quality + +We try to keep up code quality as high as practically possible. For that reason, the following steps are implemented: + +- Testing. Xournal++ HTR uses `pytest` for unit, regression and integration tests. +- Linting. Xournal++ HTR uses `ruff` for linting and code best practises. `ruff` is implemented as git pre-commit hook. Since `ruff` as pre-commit hook is configured externally with `pyproject.toml`, you can use the same settings in your IDE (e.g. VSCode) if you wish to speed up the process. +- Formatting. Xournal++ HTR uses `ruff-format` for consistent code formatting. `ruff-format` is implemented as git pre-commit hook. Since `ruff-format` as pre-commit hook is configured externally with `pyproject.toml`, you can use the same settings in your IDE if you wish to speed up the process. \ No newline at end of file diff --git a/docs/data_collection.md b/docs/data_collection.md new file mode 100644 index 0000000000000000000000000000000000000000..1af84fc0feca2cfe829171d85fc12257315a1528 --- /dev/null +++ b/docs/data_collection.md @@ -0,0 +1,13 @@ +# Data collection and annotation + +
+ + + +
+ +(Click here to get to video on YouTube.) + +
+ +TODO \ No newline at end of file diff --git a/docs/datasets_literature_review.md b/docs/datasets_literature_review.md new file mode 100644 index 0000000000000000000000000000000000000000..68d2169ede9d23cc973dad1650799ecd153a5dc8 --- /dev/null +++ b/docs/datasets_literature_review.md @@ -0,0 +1,25 @@ +THIS DOCUMENT IS WORK IN PROGRESS AND WILL BE COMPLETED LATER ON! + +## Draft content + +In this document, I am checking lit rev for datasets to know what is around and what might need to be created for best performing models. + +TODO - *Now it gets messy*: + +- See https://chatgpt.com/c/68037a32-e49c-8009-9629-c9d38404e42b +- https://github.com/rafaeljcdarce/HWR +- https://martin-thoma.com/write-math/ +- (ask him) Data: The data can be downloaded from write-math.com/data. I will try to keep a relatively recent version online. You can contact me if you want the latest version. However, I should note that currently (2015-04-12) this is about 3.7GB. This means sharing the data is not that easy. +- this seems to be constrained to single (latex) symbols; this conclusion is based on those presentations: + - https://raw.githubusercontent.com/MartinThoma/LaTeX-examples/refs/heads/master/presentations/Bachelor-Short/LaTeX/bachelor-short.pdf + - interesting ideas: https://raw.githubusercontent.com/MartinThoma/LaTeX-examples/refs/heads/master/presentations/Bachelor-Final-Presentation/LaTeX/Bachelor-Final-Presentation.pdf +- similar to: https://detexify.kirelabs.org/classify.html +- ask him about write-math.com; https://martin-thoma.com/write-math/#data +- https://arxiv.org/abs/1511.09030 +- https://hwrt.readthedocs.io/ +- https://github.com/MartinThoma/hwr-experiments +- https://hwrt.readthedocs.io/index.html +- ! https://www.reddit.com/r/selfhosted/comments/1doy32j/document_scanning_ocr_that_works_well_with/ + - https://www.reddit.com/r/computervision/comments/15er2y7/2023_review_of_tools_for_handwritten_text/ +- https://detexify.kirelabs.org/classify.html +- https://github.com/kirel/detexify-data \ No newline at end of file diff --git a/docs/developer_guide.md b/docs/developer_guide.md new file mode 100644 index 0000000000000000000000000000000000000000..b64e1df30b2cb55a24eb41717d3c4f689f3c5245 --- /dev/null +++ b/docs/developer_guide.md @@ -0,0 +1,59 @@ +# Developer Guide + +## Project design + +The design of Xournal++ HTR tries to bridge the gap between both delivering a production ready product and allowing contributors to experiment with new algorithms. + +The project design involves a Lua plugin and a Python backend, see the following figure. First, the production ready product is delivered by means of an Xournal++ plugin. The plugin is fully integrated in Xournal++ and calls a Python backend that performs the actual transcription. The Python backend allows selection of various recognition models and is thereby fully extendable with new models. + + + + + +```mermaid +sequenceDiagram + User in Xpp-->>Xpp HTR Plugin: starts transcription process using currently open file + Xpp HTR Plugin -->> Xpp HTR Lua Plugin: calls + Xpp HTR Lua Plugin -->>Xpp HTR Python Backend: constructs command using CLI + Xpp HTR Python Backend -->> Xpp HTR Python Backend: Does OCR & stores PDF + Xpp HTR Python Backend-->>User in Xpp: Gives back control to UI +``` + +Developing a usable HTR systems requires experimentation. The project structure is set up to accommodate this need. *Note that ideas on improved project structures are appreciated.* + +The experimentation is carried out in terms of "concepts". Each concept explores a different approach to HTR and possibly improves over previous concepts, but not necessarily to allow for freedom in risky experiments. Concept 1 is already implemented and uses a computer vision approach that is explained below. + +Future concepts might explore: + +- Retrain computer vision models from concept 1 using native online data representation of [Xournal++](https://github.com/xournalpp/xournalpp) +- Use sequence-to-sequence models to take advantage of native online data representation of [Xournal++](https://github.com/xournalpp/xournalpp); e.g. use [OnlineHTR](https://github.com/PellelNitram/OnlineHTR) +- Use data augmentation to increase effective size of training data +- Use of language models to correct for spelling mistakes + +### Concept 1 + +This concept uses computer vision based algorithms to first detect words on a page and then to read those words. + +The following shows a video demo on YouTube using real-life handwriting data from a Xournal file: + +[![Xournal++ HTR - Concept 1 - Demo](https://img.youtube.com/vi/FGD_O8brGNY/0.jpg)](https://www.youtube.com/watch?v=FGD_O8brGNY) + +Despite not being perfect, the main take away is that the performance is surprisingly good given that the underlying algorithm has not been optimised for Xournal++ data at all. + +**The performance is sufficiently good to be useful for the Xournal++ user base.** + +Feel free to play around with the demo yourself using [this code](https://github.com/PellelNitram/xournalpp_htr/blob/master/scripts/demo_concept_1.sh) after [installing this project](installation_user.md). The "concept 1" is also what is currently used in the plugin and shown in the [90 seconds demo](https://www.youtube.com/watch?v=boXm7lPFSRQ). + +Next steps to improve the performance of the handwritten text recognition even further could be: + +- Re-train the algorithm on Xournal++ specific data, while potentially using data augmentation. +- Use language model to improve text encoding. +- Use sequence-to-sequence algorithm that makes use of [Xournal++](https://github.com/xournalpp/xournalpp)'s data format. This translates into using online HTR algorithms. + +I would like to acknowledge [Harald Scheidl](https://github.com/githubharald) in this concept as he wrote the underlying algorithms and made them easily usable through [his HTRPipeline repository](https://github.com/githubharald/HTRPipeline) - after all I just feed his algorithm [Xournal++](https://github.com/xournalpp/xournalpp) data in concept 1. [Go check out his great content](https://githubharald.github.io/)! \ No newline at end of file diff --git a/docs/developing_new_models.md b/docs/developing_new_models.md new file mode 100644 index 0000000000000000000000000000000000000000..3b7c94a562be7f6df57fd49e5d7ef72f6628ea72 --- /dev/null +++ b/docs/developing_new_models.md @@ -0,0 +1,21 @@ +# Developing new models + + +
+ + + +
+ +(Click here to get to video on YouTube.) + +
+ +- I provide dataset and code to experiment w/ new models +- train both your own bespoke and general models. + +## Training + +### Installation + +Follow the above installation procedure and replace the step `pip install -r requirements.txt` by both `pip install -r requirements.txt` and `pip install -r requirements_training.txt` to install both the inference and training dependencies. \ No newline at end of file diff --git a/docs/funding.md b/docs/funding.md new file mode 100644 index 0000000000000000000000000000000000000000..96d84c05803e7a060c43afa3696c68274e8e471a --- /dev/null +++ b/docs/funding.md @@ -0,0 +1,9 @@ +# Funding + +This project is mostly a solo project and I love to work on it (*please [contribute](contributing.md), if you want to - happy to help along the way!*). + +However, it is both a large time commitment and requires compute resources for training models. + +If you think this project is valuable and want to express your gratitute, then please feel free to buy me a virtual coffee [here](https://ko-fi.com/martin_l) :-). + +Thanks!! \ No newline at end of file diff --git a/docs/huggingface_docker_space_deployment.md b/docs/huggingface_docker_space_deployment.md new file mode 100644 index 0000000000000000000000000000000000000000..e76cb437bd3079a103fd402d8174afc25ec787cc --- /dev/null +++ b/docs/huggingface_docker_space_deployment.md @@ -0,0 +1,52 @@ +## Local Docker image building + +1. Build the Docker image: `docker build -t xournalpp_htr .` +2. Run Docker image: `docker run -d -p 7860:7860 xournalpp_htr` + - Interactively for debugging: `docker run -it --entrypoint bash xournalpp_htr` +3. Run Docker image for interactive development + - Start docker container: `docker run -it -p 7860:7860 -v $(pwd):/temp_code_mount --entrypoint bash xournalpp_htr` + - Call Python code inside the container: `python /temp_code_mount/scripts/demo.py` + +Generally, tidy up Docker caches with `docker system prune` if your system is full. + +## looking into adding xournalpp to the image b/c i need that for the prediction (to convert xoj/xopp to pdf): + +now cross compiled on M4 +- build image: `docker buildx build --platform linux/amd64 -t xournalpp_htr .` +- interactively entering: `docker run -it --platform linux/amd64 -p 7860:7860 -v $(pwd):/temp_code_mount --entrypoint bash xournalpp_htr` +- dl deb file: `wget --no-check-certificate https://github.com/xournalpp/xournalpp/releases/download/v1.2.8/xournalpp-1.2.8-Debian-bookworm-x86_64.deb` + - there're issues!! +- alternative: use appimage: + - `wget --no-check-certificate https://github.com/xournalpp/xournalpp/releases/download/v1.2.8/xournalpp-1.2.8-x86_64.AppImage` + +## Commands to set up Supabase for event logging and data storage + +Contents of `.env` file: + +```bash +DEMO=1 +SB_URL="https://.supabase.co" +SB_KEY="" +SB_BUCKET_NAME="xournalpp_htr_hf_space" +SB_SCHEMA_NAME="public" +SB_TABLE_NAME="xournalpp_htr_hf_space_events" +``` + +Create the events table: + +```sql +create table public.xournalpp_htr_hf_space_events ( + id bigserial primary key, + timestamp timestamptz not null, + demo boolean not null, + session_id text not null, + donate_data bool not null, + interaction text not null +); +``` + +Create bucket: + +``` +xournalpp_htr_hf_space +``` \ No newline at end of file diff --git a/docs/images/.gitkeep b/docs/images/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..0b8f87f3d207083442af3345e7ae8335ffc4f92a --- /dev/null +++ b/docs/images/.gitkeep @@ -0,0 +1 @@ +# Put all images here diff --git a/docs/images/TODO.md b/docs/images/TODO.md new file mode 100644 index 0000000000000000000000000000000000000000..e62fda143c6062a7a7d95ebb9a892ee48e22b5f1 --- /dev/null +++ b/docs/images/TODO.md @@ -0,0 +1 @@ +- Add 90s qiuckstart video and document. diff --git a/docs/images/system_design.jpg b/docs/images/system_design.jpg new file mode 100644 index 0000000000000000000000000000000000000000..f08948bf8773584fda5705ae213a2f0794295825 Binary files /dev/null and b/docs/images/system_design.jpg differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000000000000000000000000000000000000..767aee9918db42ba78baa75331039c17753d7b05 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,53 @@ +# Xournal++ HTR + +Developing [handwritten text recognition](https://en.wikipedia.org/wiki/Handwriting_recognition) for [Xournal++](https://github.com/xournalpp/xournalpp). + +*Your contributions are greatly appreciated!* + +## Xournal++ HTR in 90 seconds + + + +## Why Handwritten Text Recognition for Xournal++? + +A key benefit of digital note-taking is searchability, which digital handwritten notes lack +without [handwritten text recognition (HTR)](https://en.wikipedia.org/wiki/Handwriting_recognition). +While many commercial apps offer this feature, no open-source, privacy-focused handwriting +app does - until now. + +The **Xournal++ HTR** project aims to bring on-device handwriting recognition to +[Xournal++](https://xournalpp.github.io/), a leading open-source note-taking platform. +This will make handwritten notes searchable while ensuring user privacy through local data +processing. + +## Content of these websites + +These websites document Xournal++ HTR. In the navigation bar, you can find instructions on +how to install the project, use the project and more advanced topics like how you can contribute +code and own models. In the future, many of the documents will come with small videos to get you going quicker. + + + + +## Cite + +If you are using Xournal++ HTR for your research, I'd appreciate if you could cite it. Use: + +``` +@software{Lellep_Xournalpp_HTR, + author = {Lellep, Martin}, + title = {xournalpp_htr}, + url = {https://github.com/PellelNitram/xournalpp_htr}, + license = {GPL-2.0}, +} +``` + +*(Also please consider starring the project on GitHub.)* \ No newline at end of file diff --git a/docs/installation_developer.md b/docs/installation_developer.md new file mode 100644 index 0000000000000000000000000000000000000000..5c1757de9e0b6a440aea2beebd00a092899e7757 --- /dev/null +++ b/docs/installation_developer.md @@ -0,0 +1,7 @@ +# Development installation + +1. Perform the same installation steps as described in the [user installation manual](installation_user.md). +2. Then, install developer dependencies: `pip install -r requirements_training.txt`. + +Depending on your needs, it is probably worth creating a dedicated Python environment for development. To do +so, simply change `xournalpp_htr` from [user installation manual](installation_user.md) to another name like `xournalpp_htr_dev` when you follow the above development installation steps. \ No newline at end of file diff --git a/docs/installation_user.md b/docs/installation_user.md new file mode 100644 index 0000000000000000000000000000000000000000..62198c97410098ce7950587bb30234aa3a898a79 --- /dev/null +++ b/docs/installation_user.md @@ -0,0 +1,29 @@ +# Installation + +This project consists of both the inference and training code. Most users will only be interested in the inference part, so that the below only comprises of the inference part that you need to execute the plugin from within Xournal++. + +The training part is optional and allows to help to train our own models which improve over time. This installation process is optional and detailed in [the developer guide](developer_guide.md#Installation). + +## Linux + +Run `bash INSTALL_LINUX.sh` from repository root directory. + +This script also installs the plugin as explained in the last point of the cross-platform installation procedure. The installation of the plugin is performed with `plugin/copy_to_plugin_folder.sh`, which can also be invoked independently of `INSTALL_LINUX.sh` for updating the plugin installation. + +## Cross-platform + +If you want to install the plugin manually, then execute the following commands: + +1. Create an environment: ``conda create --name xournalpp_htr python=3.10.11``. +2. Use this environment: ``conda activate xournalpp_htr``. +3. Install [HTRPipelines](https://github.com/githubharald/HTRPipeline) package using [its installation guide](https://github.com/githubharald/HTRPipeline/tree/master#installation). +4. Install all dependencies of this package ``pip install -r requirements.txt``. +5. Install the package in development mode with ``pip install -e .`` (do not forget the dot, '.'). +6. Install pre-commit hooks with: `pre-commit install`. +7. Copy `plugin/` folder content to `${XOURNAL_CONFIG_PATH}/plugins/xournalpp_htr/` with `${XOURNAL_CONFIG_PATH}` being the configuration path of Xournal++, see Xournal++ manual [here](https://xournalpp.github.io/guide/file-locations/). +8. Edit `config.lua`, setting `_M.python_executable` to your python executable **in the conda environment** and `_M.xournalpp_htr_path` to the absolute path of this repo. See the example config for details in `plugin/config.lua`. +9. Ensure Xournal++ is on your `PATH`. See [here](https://xournalpp.github.io/guide/file-locations/) for the binary location. + +## After installation + +Confirm that the installation worked by running `make tests-installation` from repository root directory. \ No newline at end of file diff --git a/docs/pyinstaller_experiment.md b/docs/pyinstaller_experiment.md new file mode 100644 index 0000000000000000000000000000000000000000..fa70deae893d6b9bba4104ab2d9749f69a6a4f97 --- /dev/null +++ b/docs/pyinstaller_experiment.md @@ -0,0 +1,23 @@ +# TODO! + +# PyInstaller Experiment + +For easier installation. + +Scope: On Linux. + +Commands I experimented with: + +```bash +cd xournalpp_htr +pyinstaller --onefile --add-data "../external/htr_pipeline/HTRPipeline/htr_pipeline/models:htr_pipeline/models" --hidden-import "PIL._tkinter_finder" run_htr.py +dist/run_htr --input-file /home/martin/data/xournalpp_htr/test_1.xoj --output-file /home/martin/Development/xournalpp_htr/tests/test_1_from_Xpp-3.pdf +``` + +This seems to work on my Ubuntu PC. + +Open questions: +- Does it work on other linux computers? + - Idea: check w/ EC2/GCP-VM instances. +- How to include the `xournalpp` binary in order to export the `xopp` file to a PDF? + - Idea: Let the use select the `xournalpp` path? \ No newline at end of file diff --git a/docs/requirements.txt b/docs/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..7c73b0972a67fe4f00f98e3cfbaf21846f65f126 --- /dev/null +++ b/docs/requirements.txt @@ -0,0 +1,3 @@ +mkdocs +mkdocs-material +mkdocs-git-revision-date-localized-plugin \ No newline at end of file diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000000000000000000000000000000000000..9ab0c36cdca7137dceb796e043767c20c86b0cc8 --- /dev/null +++ b/docs/roadmap.md @@ -0,0 +1,77 @@ +On this page, we outline the project's intended roadmap. This plan helps us strategically manage our time and resources. + +Below, we present our roadmap. It may evolve over time, so we will preserve previous versions to maintain transparency. + +## Roadmap as of *2025-05-03* + +### Visual Overview + +```mermaid +flowchart LR + A0( + Conduct + dataset + research + ) + A( + Reimplement + htr_pipeline + ) + B( + Classic algos w + OnlineHTR + ) + C( + Start own + modeling + ) + D( + Introduce + quality + measures + ) + E( + Graph NN w + OnlineHTR + ) + F( + Make + installation + easier + ) + G( + Explore offline + recognition models + like CRAFT + ) + A --> F + F --> D + D --> A0 + A0 --> C + C --> B + C --> E + C --> G +``` + +### Explanation + +This project has many potential directions, with the primary goal of delivering optimal value to users. While we are eager to implement advanced machine learning algorithms, we must first focus on usability improvements. + +Our main mid-term objective is to simplify the installation process, as users have reported it is too complex. + +Explanation of the steps: + +- **Reimplement [htr_pipeline](https://github.com/githubharald/HTRPipeline):** + We currently use the excellent [htr_pipeline](https://github.com/githubharald/HTRPipeline) by [Harald Scheidl](https://github.com/githubharald) for machine learning, but it being an external dependency complicates installation and them hosting model weights on Dropbox is not suitable for our needs. To address this, we plan to integrate these models directly into our project. Since the original repository lacks a license, we'll implement our own version, drawing inspiration from the existing work. This approach will deliver an easy-to-install product quickly, as we already know the requirements & model details. Additionally, it enhances our understanding of training models for both online and offline handwriting data. With our own models, we'll automate model retrieval and establish a model registry, likely using [Hugging Face](https://huggingface.co/), as part of adhering to MLOps best practices. Experimentation with new algorithms will benefit from the model registry and will occur subsequently, as it is more time-consuming. + +- **Make installation easier:** + We aim to make the installation process seamless across platforms, including Linux and Windows, with future support for Mac if access becomes available to us. Implementing a model registry will streamline model management and deployment, aiding future model development and enhancing ease of use while aligning with best practices. + +- **Introduce quality measures:** + To identify the best model, we need to quantify performance. Ideally, one metric will suffice, but two may be necessary if recognition and transcription remain separate tasks. + +- **Classic algos w [OnlineHTR](https://github.com/PellelNitram/OnlineHTR):** + The plan is to use [OnlineHTR](https://github.com/PellelNitram/OnlineHTR) for transcription alongside classical (non-data-driven) algorithms for recognition. + +- **Graph NN w [OnlineHTR](https://github.com/PellelNitram/OnlineHTR):** + We aim to use [OnlineHTR](https://github.com/PellelNitram/OnlineHTR) for transcription and a graph neural network for recognition. This approach seeks to develop a high-performing model that operates on the native online representation of handwriting. \ No newline at end of file diff --git a/docs/user_guide.md b/docs/user_guide.md new file mode 100644 index 0000000000000000000000000000000000000000..81e4dbd4d3a84543d7f22d7122029e70469543b5 --- /dev/null +++ b/docs/user_guide.md @@ -0,0 +1,21 @@ +# Usage + +The usage of the project is fairly simple. First, there is a Python script that performs the actual work & is useful for headless operations like batch processing. Second, and probably much more useful for the average user, the Lua plugin can be used from within Xournal++ and invokes the aforementioned Python script under the hood. + +## The Lua plugin + +Details relevant for usage of the Lua plugin: + +1. Make sure to save your file in Xournal++ beforehand. The plugin will also let you know that you need to save your file first. +2. After installation, navigate to `Plugin > Xournal++ HTR` to invoke the plugin. Then select a filename and press `Save`. Lastly, wait a wee bit until the process is finished; the Xournal++ UI will block while the plugin applies HTR to your file. If you opened Xournal++ through a command-line, you can see progress bars that show the HTR process in real-time. + +Note: Currently, the Xournal++ HTR plugin requires you to use a nightly build of Xournal++ because it uses upstream Lua API features that are not yet part of the stable build. Using the officially provided Nightly AppImag, see [here](https://xournalpp.github.io/installation/linux/), is very convenient. The plugin has been tested with the following nightly Linux build of Xournal++: + +``` +xournalpp 1.2.3+dev (583a4e47) +└──libgtk: 3.24.20 +``` + +## The Python script + +It is located in `xournalpp_htr/run_htr.py` and it features a command line interface that documents the usage of the Python script. \ No newline at end of file diff --git a/experiments/2025-02-05_writing_test/index.html b/experiments/2025-02-05_writing_test/index.html new file mode 100644 index 0000000000000000000000000000000000000000..6b23b36eda44008e1972bf1fd35ce7dab10b5e0f --- /dev/null +++ b/experiments/2025-02-05_writing_test/index.html @@ -0,0 +1,17 @@ + + + + + + Handwritten Text App + + + +
+

Handwritten Text App

+ + +
+ + + \ No newline at end of file diff --git a/experiments/2025-02-05_writing_test/script.js b/experiments/2025-02-05_writing_test/script.js new file mode 100644 index 0000000000000000000000000000000000000000..79511e47c18cfb723bc666ac3d775691906208e4 --- /dev/null +++ b/experiments/2025-02-05_writing_test/script.js @@ -0,0 +1,47 @@ +document.addEventListener('DOMContentLoaded', () => { + const canvas = document.getElementById('canvas'); + const ctx = canvas.getContext('2d'); + const exportButton = document.getElementById('exportButton'); + + let drawing = false; + const strokes = []; + + canvas.addEventListener('mousedown', (e) => { + drawing = true; + const { offsetX, offsetY } = e; + const time = new Date().toISOString(); + strokes.push({ x: offsetX, y: offsetY, time }); + ctx.beginPath(); + ctx.moveTo(offsetX, offsetY); + }); + + canvas.addEventListener('mousemove', (e) => { + if (!drawing) return; + const { offsetX, offsetY } = e; + const time = new Date().toISOString(); + strokes.push({ x: offsetX, y: offsetY, time }); + ctx.lineTo(offsetX, offsetY); + ctx.stroke(); + }); + + canvas.addEventListener('mouseup', () => { + drawing = false; + ctx.closePath(); + }); + + canvas.addEventListener('mouseleave', () => { + drawing = false; + ctx.closePath(); + }); + + exportButton.addEventListener('click', () => { + const json = JSON.stringify(strokes, null, 2); + const blob = new Blob([json], { type: 'application/json' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = 'strokes.json'; + a.click(); + URL.revokeObjectURL(url); + }); +}); \ No newline at end of file diff --git a/experiments/2025-02-05_writing_test/styles.css b/experiments/2025-02-05_writing_test/styles.css new file mode 100644 index 0000000000000000000000000000000000000000..f1a7aa15ba34ced06c88b410cf3b48b6135ae76d --- /dev/null +++ b/experiments/2025-02-05_writing_test/styles.css @@ -0,0 +1,19 @@ +body { + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + margin: 0; + font-family: Arial, sans-serif; + background-color: #f0f0f0; +} + +.container { + text-align: center; +} + +canvas { + border: 1px solid #000; + background-color: #fff; + cursor: crosshair; +} \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000000000000000000000000000000000000..d62012f150bb9f68d024d9b1463edb888c5cefed --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,40 @@ +site_name: Xournal++ HTR +site_description: Developing handwritten text recognition for Xournal++ + +repo_name: PellelNitram/xournalpp_htr +repo_url: https://github.com/PellelNitram/xournalpp_htr +edit_uri: edit/master/docs/ + +strict: true + +theme: + name: material + +plugins: + - search # necessary for search to work + - git-revision-date-localized: + timezone: Europe/London + locale: en + fallback_to_build_date: false + enable_creation_date: true + +nav: + - Introduction: 'index.md' + - Getting Started as User: + - Installation: 'installation_user.md' + - User Guide: 'user_guide.md' + - Getting Started as Developer: + - Installation: 'installation_developer.md' + - Developer Guide: 'developer_guide.md' + # - Data Collection: 'data_collection.md' # Unclear if even needed + # - Developing New Models: 'developing_new_models.md' # Very unclear what to write as I haven't built anything yet + - Contributing: 'contributing.md' + - Roadmap: 'roadmap.md' + - Funding: 'funding.md' + +markdown_extensions: + - pymdownx.superfences: # To enable mermaid.js charts, see https://squidfunk.github.io/mkdocs-material/reference/diagrams/. + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format \ No newline at end of file diff --git a/notebooks/experiment_with_IAM_OnDo_dataset.ipynb b/notebooks/experiment_with_IAM_OnDo_dataset.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..5151ac8e7d7b9a2df217db3cb1fbc1824d1904fd --- /dev/null +++ b/notebooks/experiment_with_IAM_OnDo_dataset.ipynb @@ -0,0 +1,329 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "1d03e361-cb11-49aa-9cf7-5e0a590186c5", + "metadata": {}, + "source": [ + "# Experiment w IAM OnDo dataset\n", + "\n", + "That is b/c it potentially comes with segmented word information, which is useful for a revised WordDetectorNN network.\n", + "\n", + "- [great for viewing XML files in formatted way](https://jsonformatter.org/xml-viewer/475e9e).\n", + "- [interesting package](https://github.com/RobinXL/inkml2img/blob/master/inkml2img.py)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d1e6e7ca-882c-46a2-a4c0-79ae770b0b3a", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bda86c1-cb9c-45af-a7f0-4a010c6a8a1e", + "metadata": {}, + "outputs": [], + "source": [ + "BASE_PATH = Path(\"/home/martin/Development/xournalpp_htr/data/datasets/IAMonDo-db-1.0/\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "99d9e548-c8cf-43ec-9e1d-eb2327cdb828", + "metadata": {}, + "outputs": [], + "source": [ + "inkml_path = BASE_PATH / \"001e.inkml\"" + ] + }, + { + "cell_type": "markdown", + "id": "f30ff098-9723-408c-97cd-1bfcbb672c7c", + "metadata": {}, + "source": [ + "*side idea: build InkML class! it'd be cool to make package from that and maybe publish it.*" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1cc46913-7eeb-4dc8-a19a-874ab6b5d6a5", + "metadata": {}, + "outputs": [], + "source": [ + "import xml.etree.ElementTree as ET\n", + "\n", + "tree = ET.parse(inkml_path)\n", + "root = tree.getroot()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27998475-db52-4e4b-9b18-63d5e5e64f56", + "metadata": {}, + "outputs": [], + "source": [ + "root" + ] + }, + { + "cell_type": "markdown", + "id": "6ac612f7-3187-4851-bcd8-6c022380d2a5", + "metadata": {}, + "source": [ + "Explore `root` w [this](https://docs.python.org/3/library/xml.etree.elementtree.html):" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b1cbbb3e-1bcc-4360-9114-65f791b5b413", + "metadata": {}, + "outputs": [], + "source": [ + "root.tag, root.attrib" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43cb98e1-b146-4bdf-89b3-d23089434570", + "metadata": {}, + "outputs": [], + "source": [ + "for child in root:\n", + " print(child.tag, child.attrib)" + ] + }, + { + "cell_type": "markdown", + "id": "83de1f4d-e142-4b48-ba64-b7d623015754", + "metadata": {}, + "source": [ + "indeed, the above is the content of the file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4c17d724-3916-4c62-9ea0-868d891f396d", + "metadata": {}, + "outputs": [], + "source": [ + "# todo: cont exploration" + ] + }, + { + "cell_type": "markdown", + "id": "7ef3f07e-dc5e-423b-a415-174696d5d5ca", + "metadata": {}, + "source": [ + "## experiment w/ loading both stroke and corresponding text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b089ed32-f24b-4f89-94af-5f7cbe5c56ec", + "metadata": {}, + "outputs": [], + "source": [ + "traceView = root[-1] # to access `traceView`\n", + "traceView" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "03fd3ed6-695b-4b4f-b70a-d9ba1d6fd4e1", + "metadata": {}, + "outputs": [], + "source": [ + "traceView" + ] + }, + { + "cell_type": "markdown", + "id": "a8b3635f-269d-4dd8-981a-36b7553e5576", + "metadata": {}, + "source": [ + "`textblock` and `marking` seems interesting!" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5642f657-f317-448c-9339-486cab2c6063", + "metadata": {}, + "outputs": [], + "source": [ + "marking = traceView[-1]\n", + "marking" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8a4e5d6b-b278-48eb-b4dc-54f22c38fb8a", + "metadata": {}, + "outputs": [], + "source": [ + "marking[0].text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45f82ba1-5328-4227-b366-12a781fbd27f", + "metadata": {}, + "outputs": [], + "source": [ + "marking[2][0].text, marking[2][1].text" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bb7f46c-d682-4bc2-9486-fa7b6038f32c", + "metadata": {}, + "outputs": [], + "source": [ + "tmp = marking[2][2]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6ba6226b-44bb-4932-82da-92773c4faeb6", + "metadata": {}, + "outputs": [], + "source": [ + "ids_to_use = []\n", + "\n", + "for x in tmp:\n", + " if x.tag == \"traceView\":\n", + " ids_to_use.append(x.attrib[\"traceDataRef\"])" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0184aded-1d9f-4bbd-b48b-11735d2b60a2", + "metadata": {}, + "outputs": [], + "source": [ + "ids_to_use" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52358cf2-585d-4130-b90b-f5e0ab5d8015", + "metadata": {}, + "outputs": [], + "source": [ + "traces_to_use = []\n", + "\n", + "for x in root.findall(\"trace\"):\n", + " id_to_check = x.attrib[\"{http://www.w3.org/XML/1998/namespace}id\"]\n", + " for y in ids_to_use:\n", + " if y[1:] == id_to_check:\n", + " traces_to_use.append([id_to_check, x.text])\n", + "\n", + "traces_to_use.sort(key=lambda x: x[0])" + ] + }, + { + "cell_type": "markdown", + "id": "997d1c64-a7e1-474e-9bd1-6567b3da317b", + "metadata": {}, + "source": [ + "get dfs of traces:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "0f35219c-1cb2-4c5f-98e6-2eecb51b16d2", + "metadata": {}, + "outputs": [], + "source": [ + "dfs = []\n", + "\n", + "for name, trace in traces_to_use:\n", + " print(name)\n", + " trace = [\n", + " [float(yy) for yy in xx.replace(\"-\", \" -\").split()]\n", + " for xx in trace.split(\",\")\n", + " if xx[0] not in [\"'\", '\"']\n", + " ]\n", + " df = pd.DataFrame(data=trace, columns=[\"x\", \"y\", \"t\", \"f\"])\n", + "\n", + " dfs.append(df)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c4aa0a02-3089-4cdc-9a9f-db749e515573", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "for df in dfs:\n", + " plt.scatter(df.cumsum()[\"x\"], df.cumsum()[\"y\"])\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "d304a1f8-dbee-4d25-b264-ecb9b74d2838", + "metadata": {}, + "source": [ + "ok, apparently i have no idea what I am plotting :-D" + ] + }, + { + "cell_type": "markdown", + "id": "143fbc12-f553-4bba-b61c-269000872a3e", + "metadata": {}, + "source": [ + "next steps:\n", + "- read spec of IAM On Do to learn what is stored.\n", + "- Read [this spec](https://www.w3.org/TR/InkML/#trace) to understand the above cryptic string and then plot it to see if it suits my needs of segmented word data." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3 (ipykernel)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/experiment_with_clustering_for_online_word_detection.ipynb b/notebooks/experiment_with_clustering_for_online_word_detection.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..4c0f23d9f0b68ebd47ee5a80c9a7ec7067531266 --- /dev/null +++ b/notebooks/experiment_with_clustering_for_online_word_detection.ipynb @@ -0,0 +1,526 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Experiment w clustering for online word detection" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import matplotlib.patches as patches\n", + "import matplotlib.pyplot as plt\n", + "import numpy as np\n", + "import pandas as pd\n", + "from sklearn.cluster import (\n", + " DBSCAN,\n", + " HDBSCAN,\n", + " AffinityPropagation,\n", + " AgglomerativeClustering,\n", + " MeanShift,\n", + " SpectralClustering,\n", + ")\n", + "from sklearn.metrics import adjusted_rand_score\n", + "\n", + "from xournalpp_htr.training.io import load_list_of_bboxes\n", + "from xournalpp_htr.training.visualise import plot_clustered_document" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Experiment structure\n", + "\n", + "### Hypothesis\n", + "\n", + "One can find an algorithm that segments strokes into words using my handwriting.\n", + "\n", + "Side note: This is useful b/c I can then use [OnlineHTR](https://github.com/PellelNitram/OnlineHTR) to transcribe the words.\n", + "\n", + "### Notebook structure\n", + "\n", + "1. Load data, incl ground truth.\n", + "2. Pre-compute a set of features. Later, feature engineering might be added.\n", + "3. Iterate over a few algorithms and measure their performance using the ground truth.\n", + "\n", + "Alternative addition later on: Manually remove strokes that're too long (in distribution sense) or too straight. That is another step because it will require a dataset with such strokes that don't belong to words." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "## Settings\n", + "\n", + "OUTPUT_PATH = Path(\"experiment_results\")\n", + "OUTPUT_PATH.mkdir(parents=True, exist_ok=True)\n", + "\n", + "PLOT_RESULTS = True\n", + "PLOT_RESULTS = False" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Helper functions" + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Add here if necessary." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Load annotations" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Previously, I loaded the data as `XournalppDocument` but that approach lacked ground truth data. Instead, I now load the annotated data, which comes with ground truth data." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [], + "source": [ + "annotated_bboxes = load_list_of_bboxes(\n", + " \"../tests/data/2024-10-13_minimal.annotations.json\"\n", + ")\n", + "\n", + "DPI = 72 # TODO: Add this to annotations!\n", + "\n", + "# TODO: Maybe integrate `/DPI` into the x and y values? Maybe convert to cm?\n", + "# TODO: Add page dimensions, i.e.:\n", + "# - float(page.meta_data[\"width\"]) / DPI,\n", + "# - float(page.meta_data[\"height\"]) / DPI," + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# ========\n", + "# Figure 1\n", + "# ========\n", + "\n", + "length = len(annotated_bboxes[\"bboxes\"])\n", + "nr_2 = 4\n", + "nr_1 = length // nr_2 + 1\n", + "\n", + "fig, axes = plt.subplots(nrows=nr_1, ncols=nr_2, figsize=(10, 8))\n", + "\n", + "for i_bbox in range(length):\n", + " bbox = annotated_bboxes[\"bboxes\"][i_bbox]\n", + "\n", + " a = axes.flatten()[i_bbox]\n", + "\n", + " a.set_aspect(\"equal\")\n", + " a.set_title(bbox[\"text\"])\n", + " a.set_xlabel(\"x\")\n", + " a.set_ylabel(\"-y\")\n", + "\n", + " for bbox_stroke in bbox[\"bbox_strokes\"]:\n", + " x = bbox_stroke[\"x\"] / DPI\n", + " y = bbox_stroke[\"y\"] / DPI\n", + " a.scatter(x, -y, c=\"black\", s=1)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()\n", + "\n", + "# ========\n", + "# Figure 2\n", + "# ========\n", + "\n", + "plt.figure(figsize=(10, 8))\n", + "\n", + "a = plt.gca()\n", + "a.set_aspect(\"equal\")\n", + "a.set_xlabel(\"x\")\n", + "a.set_ylabel(\"-y\")\n", + "\n", + "for i_bbox in range(length):\n", + " bbox = annotated_bboxes[\"bboxes\"][i_bbox]\n", + "\n", + " # Draw bbox\n", + " xy = (\n", + " min([bbox[\"point_1_x\"], bbox[\"point_2_x\"]]) / DPI,\n", + " min([-bbox[\"point_1_y\"], -bbox[\"point_2_y\"]])\n", + " / DPI, # TODO: This messing around w/ y coord sign is annoying\n", + " )\n", + " dx = np.abs(bbox[\"point_1_x\"] - bbox[\"point_2_x\"]) / DPI\n", + " dy = np.abs(bbox[\"point_1_y\"] - bbox[\"point_2_y\"]) / DPI\n", + " a.add_patch(\n", + " patches.Rectangle(xy, dx, dy, linewidth=1, edgecolor=\"r\", facecolor=\"none\")\n", + " )\n", + "\n", + " # Draw label\n", + " a.text(x=xy[0], y=xy[1] + dy, s=bbox[\"text\"], c=\"red\")\n", + "\n", + " for bbox_stroke in bbox[\"bbox_strokes\"]:\n", + " x = bbox_stroke[\"x\"] / DPI\n", + " y = bbox_stroke[\"y\"] / DPI\n", + " a.scatter(x, -y, c=\"black\", s=1)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Prepare list of all strokes w/ relevant meta information as ground truth. This variable serves as training data:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "df_strokes_data = {\n", + " \"x\": [],\n", + " \"y\": [],\n", + " \"x_mean\": [],\n", + " \"y_mean\": [],\n", + " \"i_bbox\": [],\n", + " \"text\": [],\n", + "}\n", + "\n", + "for i_bbox in range(len(annotated_bboxes[\"bboxes\"])):\n", + " bbox = annotated_bboxes[\"bboxes\"][i_bbox]\n", + "\n", + " for bbox_stroke in bbox[\"bbox_strokes\"]:\n", + " x = +bbox_stroke[\"x\"] / DPI\n", + " y = -bbox_stroke[\"y\"] / DPI\n", + "\n", + " df_strokes_data[\"x\"].append(x)\n", + " df_strokes_data[\"y\"].append(y)\n", + " df_strokes_data[\"x_mean\"].append(np.mean(x))\n", + " df_strokes_data[\"y_mean\"].append(np.mean(y))\n", + " df_strokes_data[\"i_bbox\"].append(i_bbox)\n", + " df_strokes_data[\"text\"].append(bbox[\"text\"])\n", + "\n", + "df_train = pd.DataFrame.from_dict(df_strokes_data)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Plot the training data:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure(figsize=(10, 8))\n", + "\n", + "a = plt.gca()\n", + "a.set_aspect(\"equal\")\n", + "a.set_xlabel(\"x\")\n", + "a.set_ylabel(\"y\")\n", + "\n", + "for (i_bbox, text), df_grouped in df_train.groupby(\n", + " [\"i_bbox\", \"text\"],\n", + "):\n", + " a.scatter(df_grouped[\"x_mean\"], df_grouped[\"y_mean\"], c=\"red\", s=2, zorder=999)\n", + "\n", + " bottom_left_x = np.inf\n", + " bottom_left_y = np.inf\n", + " top_right_x = -np.inf\n", + " top_right_y = -np.inf\n", + " for _, row in df_grouped.iterrows():\n", + " a.plot(row.x, row.y) # , c=cmap(i_row/N))\n", + " if row.x.min() < bottom_left_x:\n", + " bottom_left_x = row.x.min()\n", + " if row.y.min() < bottom_left_y:\n", + " bottom_left_y = row.y.min()\n", + " if row.x.max() > top_right_x:\n", + " top_right_x = row.x.max()\n", + " if row.y.max() > top_right_y:\n", + " top_right_y = row.y.max()\n", + "\n", + " # Plot bounding box\n", + " xy = (bottom_left_x, bottom_left_y)\n", + " dx = top_right_x - bottom_left_x\n", + " dy = top_right_y - bottom_left_y\n", + " a.add_patch(\n", + " patches.Rectangle(xy, dx, dy, linewidth=1, edgecolor=\"r\", facecolor=\"none\")\n", + " )\n", + "\n", + " # Plot text\n", + " a.text(x=bottom_left_x, y=top_right_y, s=f'\"{text}\" ({i_bbox})', c=\"red\")\n", + "\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Iterate over clustering algorithms" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "%%time\n", + "\n", + "all_clusterings = [\n", + " AgglomerativeClustering(\n", + " n_clusters=22, distance_threshold=None\n", + " ), # I hard-code 22 b/c I counted that there're 22 clusters\n", + " AgglomerativeClustering(n_clusters=10, distance_threshold=None),\n", + " AgglomerativeClustering(\n", + " n_clusters=None, distance_threshold=1e0\n", + " ), # One could maybe tune it by investigating nr of clusters over distance threshold; TODO: Distance threshold using distribution?!\n", + " SpectralClustering(\n", + " n_clusters=15, # 21,\n", + " affinity=\"nearest_neighbors\",\n", + " ),\n", + " SpectralClustering(\n", + " n_clusters=21, # 21,\n", + " affinity=\"nearest_neighbors\",\n", + " ),\n", + " SpectralClustering(\n", + " n_clusters=6, # 21,\n", + " affinity=\"nearest_neighbors\",\n", + " ),\n", + " MeanShift(\n", + " bandwidth=None,\n", + " ),\n", + " MeanShift(\n", + " bandwidth=0.1,\n", + " ),\n", + " MeanShift(\n", + " bandwidth=1.0,\n", + " ),\n", + " MeanShift(\n", + " bandwidth=10.0,\n", + " ),\n", + " AffinityPropagation(),\n", + " HDBSCAN(min_cluster_size=2),\n", + " # FeatureAgglomeration(\n", + " # n_clusters=None,\n", + " # distance_threshold=0.1,\n", + " # ),\n", + " # FeatureAgglomeration(\n", + " # n_clusters=None,\n", + " # distance_threshold=1.0,\n", + " # ),\n", + " # FeatureAgglomeration(\n", + " # n_clusters=None,\n", + " # distance_threshold=10.0,\n", + " # ),\n", + "]\n", + "\n", + "all_clusterings += [DBSCAN(eps) for eps in np.logspace(-4, 1, 1000)]\n", + "all_clusterings += [\n", + " AgglomerativeClustering(n_clusters=None, distance_threshold=DISTANCE_THRESHOLD)\n", + " for DISTANCE_THRESHOLD in np.logspace(-4, 1, 1000)\n", + "]\n", + "\n", + "results = {\n", + " \"index\": [],\n", + " \"score\": [],\n", + "}\n", + "for i_clustering, clustering in enumerate(all_clusterings):\n", + " print(i_clustering, clustering)\n", + " clustering.fit(df_train[[\"x_mean\", \"y_mean\"]])\n", + "\n", + " score = adjusted_rand_score(df_train[\"i_bbox\"], clustering.labels_)\n", + "\n", + " results[\"index\"].append(i_clustering)\n", + " results[\"score\"].append(score)\n", + "\n", + " # Plotting\n", + " if PLOT_RESULTS:\n", + " fig, [a_ground_truth, a_predicted] = plt.subplots(1, 2, figsize=(10, 8))\n", + " plot_clustered_document(\n", + " a_ground_truth,\n", + " a_predicted,\n", + " clustering,\n", + " annotated_bboxes,\n", + " DPI,\n", + " df_train,\n", + " a_predicted_title=f\"A-RAND={score}\",\n", + " )\n", + " plt.savefig(OUTPUT_PATH / f\"iClustering{i_clustering}.png\")\n", + " plt.close()\n", + "\n", + "results = pd.DataFrame.from_dict(results)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "\n", + "plt.scatter(results[\"index\"], results[\"score\"], c=\"red\")\n", + "\n", + "plt.xlabel(\"Index of clustering settings\")\n", + "plt.ylabel(\"Adjusted Rand Score (larger is better)\")\n", + "plt.savefig(\"2024-10-18_clustering_experiments.png\", dpi=200)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, check if the clusters make sense by plotting the clusters on the page of a set of pre-selected settings to test out:" + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO!!!\n", + "\n", + "# CONTINUE TO WORK HERE!!!" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "TODO: Learning: The peak at ~800 seems to classify rows of text. This should be fine w/ OnlineHTR!" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "# TODO: Add more stroke features. Then run large screen. Also add feature selection.\n", + "# TODO: Maybe add k fold?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, plot the dendrogram, see [here](https://scikit-learn.org/stable/auto_examples/cluster/plot_agglomerative_dendrogram.html#sphx-glr-auto-examples-cluster-plot-agglomerative-dendrogram-py)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Next, try out DBSCAN! Also see [here](https://scikit-learn.org/stable/modules/clustering.html)." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Also next, try out another document to play around with." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Question: Is my OnlineHTR model robust against rotated text?! Maybe one should rotate the text first?" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Note: It is probably worth it to write a bit of infrastructure code to experiment more (and easier and easier to compare) with these clustering approaches.\n", + "\n", + "Next: Feed these sequences to `OnlineHTR` or retrained `SimpleHTR` nmodel." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## TODOs:\n", + "\n", + "- I think next cool thing to try out is to do proper feature engineering to try to enhance the features. Using the raw strokes could be regarded as last resort but IMHO doesn't make sense b/c a stroke always has a single word attached as strokes cannot be split, which they could be if one allows clusterings on the raw datapoints instead of strokes.\n", + "\n", + "- Good source for rand score: [see here](https://stats.stackexchange.com/questions/260229/comparing-a-clustering-algorithm-partition-to-a-ground-truth-one).\n", + "- After finding the best clustering, do apply OnlineHTR to check how it performs!\n", + "- To overcome the scale issue (i.e. everyone's handwriting scale is a wee bit different), one would need to use an approach that is based on 'nearest neighbours'. This works b/c one does not write on top of existing words.\n", + " - also, one could weight the x direction more in definition of closeness/distance\n", + "- Hook up OnlineHTR to here!\n", + "- I think the biggest problem for the OnlineHTR model would be the different line positions based on the way it was trained. Hence, one could maybe put extra emphasis on clusters being on similar y values.\n", + "- I have to say that I am unclear if a heuristic (i.e. a clustering algo w/ smartly chosen parameters) is really enough. Certainly for now, but a fully data-driven way would be better to accommodate different writers. This is probably relevant for a next iteration of the model.\n", + " - E.g., is this approach robust against larger handwriting?\n", + "- Hyper parameters like distance threshold are probably a function of the content of the page (e.g. diagrams, written text height, etc).\n", + "- It would be cool to try graph NN. Also, I'd love to add more features than the mean. That might help in learning." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "xournalpp_htr", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.10.11" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/plugin/config.lua b/plugin/config.lua new file mode 100644 index 0000000000000000000000000000000000000000..97c1925a85fed8d278d07f5d242309d0b14eb368 --- /dev/null +++ b/plugin/config.lua @@ -0,0 +1,14 @@ +local _M = {} + +-- user settings +_M.python_executable = "/home/martin/anaconda3/envs/xournalpp_htr/bin/python" +_M.xournalpp_htr_path = "/home/martin/Development/xournalpp_htr/xournalpp_htr/run_htr.py" +_M.model = "dummy" +_M.output_file = "/home/martin/Development/xournalpp_htr/tests/test_1_from_Xpp.pdf" +_M.debug_HTR_command = false +-- TODO: allow UI to set other parameters as well of `xournalpp_htr`. + +-- TODO replace later w/ temp exported file - filename will be derived automatically +_M.filename = "/home/martin/Development/xournalpp_htr/tests/test_1.xoj" + +return _M \ No newline at end of file diff --git a/plugin/copy_to_plugin_folder.sh b/plugin/copy_to_plugin_folder.sh new file mode 100644 index 0000000000000000000000000000000000000000..50fa6ea47ee80ccaaacbf17693cd5b467fc0062b --- /dev/null +++ b/plugin/copy_to_plugin_folder.sh @@ -0,0 +1,16 @@ +# ======== +# SETTINGS +# ======== + +TARGET_FOLDER=~/.config/xournalpp/plugins/xournalpp_htr/ +# TARGET_FOLDER=/usr/share/xournalpp/plugins/xournalpp_htr # requires `sudo` + +# ============ +# COPY PROCESS +# ============ + +mkdir -p ${TARGET_FOLDER} + +cp plugin.ini ${TARGET_FOLDER} +cp main.lua ${TARGET_FOLDER} +cp config.lua ${TARGET_FOLDER} \ No newline at end of file diff --git a/plugin/demo_config.lua b/plugin/demo_config.lua new file mode 100644 index 0000000000000000000000000000000000000000..97c1925a85fed8d278d07f5d242309d0b14eb368 --- /dev/null +++ b/plugin/demo_config.lua @@ -0,0 +1,14 @@ +local _M = {} + +-- user settings +_M.python_executable = "/home/martin/anaconda3/envs/xournalpp_htr/bin/python" +_M.xournalpp_htr_path = "/home/martin/Development/xournalpp_htr/xournalpp_htr/run_htr.py" +_M.model = "dummy" +_M.output_file = "/home/martin/Development/xournalpp_htr/tests/test_1_from_Xpp.pdf" +_M.debug_HTR_command = false +-- TODO: allow UI to set other parameters as well of `xournalpp_htr`. + +-- TODO replace later w/ temp exported file - filename will be derived automatically +_M.filename = "/home/martin/Development/xournalpp_htr/tests/test_1.xoj" + +return _M \ No newline at end of file diff --git a/plugin/main.lua b/plugin/main.lua new file mode 100644 index 0000000000000000000000000000000000000000..d3b2bc82adcab678940cbe0ba66d5d5d49900942 --- /dev/null +++ b/plugin/main.lua @@ -0,0 +1,46 @@ +function initUi() + app.registerUi({["menu"] = "Xournal++ HTR", ["callback"] = "run", ["accelerator"] = "F1"}); +end + +function save_file(path) + if path:len() > 0 then + + -- Read settings: I use this (https://stackoverflow.com/a/41176958). An + -- alternative could have been https://stackoverflow.com/a/41176826. Both + -- found using G"lua read settings file". + local config = require "config" + + config.filename = '"' .. app.getDocumentStructure()['xoppFilename'] .. '"' + config.output_file = '"' .. path .. '"' + + command = config.python_executable .. " " .. config.xournalpp_htr_path + .. " -if " .. config.filename + .. " -of " .. config.output_file + if config.debug_HTR_command then + print(command) + else + os.execute(command) + end + + end +end + +function run() + + document_structure = app.getDocumentStructure() + + if document_structure['xoppFilename']:len() == 0 then + app.openDialog('Please save document prior to exporting it as searchable PDF!', {"Ok"}, "", true) + else + app.fileDialogSave("save_file", "untitled.pdf") + end + +end + +-- TODO: Think of workflow to maximise usability for user +-- TODO: How to store settings? Ideally permanently? +-- TODO: Interesting code from example plugins: +-- - Get filename: https://github.com/xournalpp/xournalpp/blob/master/plugins/Export/main.lua#L29 +-- - Toggle logic: https://github.com/xournalpp/xournalpp/blob/master/plugins/HighlightPosition/main.lua#L5 +-- - UI: https://github.com/xournalpp/xournalpp/blob/master/plugins/MigrateFontSizes/main.lua +-- - OS interaction: https://github.com/xournalpp/xournalpp/blob/master/plugins/QuickScreenshot/main.lua diff --git a/plugin/plugin.ini b/plugin/plugin.ini new file mode 100644 index 0000000000000000000000000000000000000000..e7f5ae7088cfa46e9002b963329c345e7bebf455 --- /dev/null +++ b/plugin/plugin.ini @@ -0,0 +1,17 @@ +## Based on this explanation: https://xournalpp.github.io/guide/plugins/plugins/ + +[about] +## Author / Copyright notice +author=Martin Lellep + +description=Developing handwritten text recognition for Xournal++ + +## If the plugin is packed with Xournal++, use +## then it gets the same version number +version=0.1 + +[default] +enabled=false + +[plugin] +mainfile=main.lua diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..336ce72a0a906eb4cf25923968c3740a1cdd104a --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,20 @@ +[tool.ruff] +fix = true +show-fixes = true +line-length = 88 +lint.select = [ + "C", # mccabe rules + "F", # pyflakes rules + "E", # pycodestyle error rules + "W", # pycodestyle warning rules + "B", # flake8-bugbear rules + "I", # isort rules +] +lint.ignore = [ + "C901", # max-complexity-10 + "E501", # line-too-long +] + +[tool.ruff.format] +indent-style = "space" +quote-style = "double" \ No newline at end of file diff --git a/pytest.ini b/pytest.ini new file mode 100644 index 0000000000000000000000000000000000000000..6524f055ae6b3f6dd88b84e8f5c1d36ac802d4fc --- /dev/null +++ b/pytest.ini @@ -0,0 +1,11 @@ +[pytest] +minversion = 6.0 +testpaths = + tests +markers = + slow: Marks tests as slow (select with '-m slow' and deselect with '-m "not slow"') + technical: Marks tests as technical tests to ensure that code features work as expected + correctness: Denotes tests that check physical behaviour and to ensure physical correctness + installation: Marks tests that confirm this package was installed correctly. + data: test data and its location. + visual_check: Marks tests that need visual checks. \ No newline at end of file diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..d38b071d7cd2eb2dfd012f7f946e9b32716d67a5 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,9 @@ +numpy +beautifulsoup4 +matplotlib +opencv-python +pytest +lxml +pymupdf +tqdm +pre-commit \ No newline at end of file diff --git a/requirements_training.txt b/requirements_training.txt new file mode 100644 index 0000000000000000000000000000000000000000..c35faef61ed76bacc5adea772a36da9f8999918c --- /dev/null +++ b/requirements_training.txt @@ -0,0 +1,4 @@ +pandas +jupyter +gradio +gitpython \ No newline at end of file diff --git a/scripts/demo.py b/scripts/demo.py new file mode 100644 index 0000000000000000000000000000000000000000..eb25f54d509bd763d2441d76c36b1e20b8efff50 --- /dev/null +++ b/scripts/demo.py @@ -0,0 +1,230 @@ +import os +import tempfile +import uuid +from datetime import datetime, timezone +from pathlib import Path + +import gradio as gr +from dotenv import load_dotenv +from pdf2image import convert_from_path +from supabase import Client, create_client + +from xournalpp_htr.documents import get_document +from xournalpp_htr.models import compute_predictions +from xournalpp_htr.utils import export_to_pdf_with_xournalpp, get_env_variable +from xournalpp_htr.xio import write_predictions_to_PDF + +load_dotenv() + +DEMO = get_env_variable("DEMO") == "1" +SB_URL = get_env_variable("SB_URL") +SB_KEY = get_env_variable("SB_KEY") +SB_BUCKET_NAME = get_env_variable("SB_BUCKET_NAME") +SB_SCHEMA_NAME = get_env_variable("SB_SCHEMA_NAME") +SB_TABLE_NAME = get_env_variable("SB_TABLE_NAME") + +# --- Image Processing Functions --- + + +def get_temporary_directory() -> Path: + return Path(tempfile.gettempdir()) + + +def get_path_of_exported_pdf(session_id: str) -> Path: + return get_temporary_directory() / f"{session_id}_input_as_pdf.pdf" + + +def get_path_of_pdf_with_htr(session_id: str) -> Path: + return get_temporary_directory() / f"{session_id}_pdf_with_htr.pdf" + + +def log_interaction( + session_id: str, + donate_data: bool, + interaction: str, + document_path: str | None, +): + supabase: Client = create_client(SB_URL, SB_KEY) + + if donate_data and document_path: + document_path = Path(document_path) + destination_path = f"{session_id}{document_path.suffix}" + with open(document_path, "rb") as file: + supabase.storage.from_(SB_BUCKET_NAME).upload( + destination_path, + file, + {"content-type": "application/octet-stream"}, + ) + + # Insert metadata row + row = { + "timestamp": datetime.now(timezone.utc).isoformat(), + "demo": DEMO, + "session_id": session_id, + "donate_data": donate_data, + "interaction": interaction, + } + + supabase.schema(SB_SCHEMA_NAME).table(SB_TABLE_NAME).insert(row).execute() + + +def upload_document(document_path, session_id: str, donate_data: bool) -> str: + log_interaction( + session_id=session_id, + donate_data=donate_data, + interaction="upload_document", + document_path=document_path, + ) + if document_path is None: + return None + return document_path + + +def document_to_image_of_first_page(document_path, session_id): + """Flips the input image horizontally.""" + log_interaction( + session_id=session_id, + donate_data=False, + interaction="document_to_image_of_first_page", + document_path=None, + ) + if document_path is None: + return None + output_path = get_path_of_exported_pdf(session_id) + export_to_pdf_with_xournalpp( + Path(document_path), + output_path, + ) + images = convert_from_path(output_path, first_page=1, last_page=1) + first_page = images[0] + return first_page + + +def document_to_HTR_document_and_image_of_first_page(document_path, session_id): + """Rotates the input image 90 degrees counter-clockwise.""" + log_interaction( + session_id=session_id, + donate_data=False, + interaction="document_to_HTR_document_and_image_of_first_page", + document_path=None, + ) + if document_path is None: + return None + document_path = Path(document_path) + input_as_pdf_path = get_path_of_exported_pdf(session_id) + pdf_with_htr = get_path_of_pdf_with_htr(session_id) + document = get_document(document_path) + predictions = compute_predictions( + model_name="2024-07-18_htr_pipeline", document=document + ) + write_predictions_to_PDF( + input_as_pdf_path, + pdf_with_htr, + predictions, + debug_htr=True, + ) # TODO: make it a generator to track progress externally like here. + images = convert_from_path(pdf_with_htr, first_page=1, last_page=1) + first_page = images[0] + return first_page + + +def save_HTR_document_for_download(session_id): + log_interaction( + session_id=session_id, + donate_data=False, + interaction="save_HTR_document_for_download", + document_path=None, + ) + pdf_with_htr = get_path_of_pdf_with_htr(session_id) + if not pdf_with_htr.exists(): + return None + return str(pdf_with_htr) + + +# --- Gradio UI Layout --- + +with gr.Blocks(theme=gr.themes.Soft()) as demo: + gr.Markdown( + """ + # [Xournal++ HTR](https://github.com/PellelNitram/xournalpp_htr) Demo + + This is an online demo of the [Xournal++ HTR](https://github.com/PellelNitram/xournalpp_htr) project, which strives to bring modern handwritten + text recognition to open-source handwritten note softwares like [Xournal++](https://xournalpp.github.io/). + + While [Xournal++ HTR](https://github.com/PellelNitram/xournalpp_htr) is natively built to be running locally, this demo deploys it online so you + can try it out without any installation. We do not collect any personal data (see [source code of this demo](https://github.com/PellelNitram/xournalpp_htr/blob/master/scripts/demo.py)) + but allow you to donate your data if you want so that we can build better underlying machine learning models for all of us (all open-source, of course!). + + Note that the HTR results are not yet perfect. This is an ongoing project and we are actively working on improving the models. + Currently, we are constrained by the limited amount of publicly available training data and by our working time (this is a hobby project next to our day jobs). + + The "we" in the paragraphs above is currently really only me, [Martin Lellep](https://lellep.xyz/?utm_campaign=xppGradioDemo), the main developer of Xournal++ HTR. I really love to work on + [Xournal++ HTR](https://github.com/PellelNitram/xournalpp_htr)! If you think this project is valuable and want to express your gratitute, then please feel free to [buy me a virtual coffee here](https://ko-fi.com/martin_l) + so that I can buy more GPU power for training models and continue to let the GPUs go brrr :-). + """ + ) + + session_id = gr.State( + value=lambda: datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + "_" + + str(uuid.uuid4()) + ) + + original_image_state = gr.State() + + donate_data_checkbox = gr.Checkbox( + label="Donate Data: Help us to improve our open-source models by donating your uploaded document. Everything will be released as open-source!", + value=False, + ) + + upload_button = gr.UploadButton( + "1. Click to Upload an XOJ File", + file_types=[".xoj", ".xopp"], + file_count="single", + ) + + with gr.Row(): + image_viewer_1 = gr.Image( + label="Original document", interactive=False, height=350 + ) + image_viewer_2 = gr.Image( + label="Document with HTR", interactive=False, height=350 + ) + + with gr.Row(): + button_1 = gr.Button("2. Export to PDF and Show First Page") + button_2 = gr.Button("3. Compute PDF with HTR and Show First Page") + + button_download = gr.Button("4. Download PDF with HTR") + file_output = gr.File(label="Download PDF with HTR") + + # --- Event Handlers --- + + upload_button.upload( + fn=upload_document, + inputs=[upload_button, session_id, donate_data_checkbox], + outputs=original_image_state, + ) + + button_1.click( + fn=document_to_image_of_first_page, + inputs=[original_image_state, session_id], + outputs=image_viewer_1, + ) + + button_2.click( + fn=document_to_HTR_document_and_image_of_first_page, + inputs=[original_image_state, session_id], + outputs=image_viewer_2, + ) + + button_download.click( + fn=save_HTR_document_for_download, + inputs=session_id, + outputs=file_output, + ) + + +if __name__ == "__main__": + port = int(os.environ.get("PORT", 7860)) # Use HF-provided port or fallback + demo.launch(server_name="0.0.0.0", server_port=port) diff --git a/scripts/demo_concept_1.sh b/scripts/demo_concept_1.sh new file mode 100644 index 0000000000000000000000000000000000000000..8635cc1e9324df3ece0380d36b078e1b6958ebbb --- /dev/null +++ b/scripts/demo_concept_1.sh @@ -0,0 +1,4 @@ +FILE=~/data/xournalpp_htr/datasets/tests/test_1.xoj +FILE=../tests/test_1.xoj + +python ../xournalpp_htr/demo_concept_1.py --input-file ${FILE} diff --git a/setup.py b/setup.py new file mode 100644 index 0000000000000000000000000000000000000000..8bdabcab2985eba60785774ffd677e5941dbbc5a --- /dev/null +++ b/setup.py @@ -0,0 +1,49 @@ +import os +import sys + +import setuptools + +## Modifies config.lua to use the appropriate paths +# Get the path of this file +htr_dir = os.path.dirname(os.path.abspath(__file__)) + +# Path to the config.lua file +config_file = os.path.join(htr_dir, "plugin", "config.lua") + +# Fix direction of slashes, needed on Windows +htr_dir = htr_dir.replace("\\", "/") + +# Get the path of the Python executable +python_executable = sys.executable.replace("\\", "/") + +# Modify the config.lua file +with open(config_file, "r") as f: + lines = f.readlines() + +# Modify the necessary lines in the config.lua file +modified_lines = [] +for line in lines: + if line.startswith("_M.python_executable ="): + modified_lines.append('_M.python_executable = "' + python_executable + '"\n') + elif line.startswith("_M.xournalpp_htr_path ="): + modified_lines.append( + '_M.xournalpp_htr_path = "' + htr_dir + '/xournalpp_htr/run_htr.py"\n' + ) + else: + modified_lines.append(line) + +# Write the modified lines back to the config.lua file +with open(config_file, "w") as f: + f.writelines(modified_lines) + +with open("README.md", "r") as fh: + long_description = fh.read() + +setuptools.setup( + name="xournalpp_htr", + version="0.0.1", + description="Developing handwritten text recognition for Xournal++.", + long_description=long_description, + long_description_content_type="text/markdown", + packages=setuptools.find_packages(), +) diff --git a/tests/.gitkeep b/tests/.gitkeep new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000000000000000000000000000000000000..2ae617f7cbe9079ec07618241f4cfd43310678d2 --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,60 @@ +import urllib.request +from pathlib import Path + +import pytest + + +@pytest.fixture +def get_repo_root_directory(request: pytest.FixtureRequest) -> Path: + """ + A `pytest` fixture that returns the root directory of the repository. + + This fixture retrieves the root directory from the pytest configuration + and asserts that the directory contains a "README.md" file. It is based + on an example provided in a + [Stack Overflow answer](https://stackoverflow.com/a/57039134). + + :param request: A `pytest` fixture that provides information about the + requesting test function. + :returns: A Path object representing the root directory of the repository. + :raises AssertionError: If the "README.md" file is not found in the root + directory. + """ + rootdir = Path(request.config.rootdir) + assert (rootdir / "README.md").is_file() + return rootdir + + +@pytest.fixture +def get_path_to_minimal_test_data(get_repo_root_directory: Path) -> Path: + """Fixture to retrieve the path to the minimal test data file. + + This function checks for the existence of a specific test data file + located at `${repo_root}/tests/data/2024-07-26_minimal.xopp`. If the + file does not exist, it is downloaded from a predefined URL and saved + at the specified location so that it can be retrieved directly next + time; i.e. it is cached. + + :param get_repo_root_directory: A fixture that provides the root + directory of the repository. + :returns: The path to the minimal test data file. + """ + + path_to_minimal_test_data = ( + get_repo_root_directory / "tests/data/2024-07-26_minimal.xopp" + ) + + if not path_to_minimal_test_data.is_file(): + url = "https://bit.ly/2024-07-26_minimal_xopp" + urllib.request.urlretrieve(url, path_to_minimal_test_data) + + return path_to_minimal_test_data + + +@pytest.fixture +def get_path_to_IAM_OnDB_dataset(get_repo_root_directory: Path) -> Path: + """TODO Add docstring.""" + + path_to_IAM_OnDB_dataset = get_repo_root_directory / "data/datasets/IAM-OnDB" + + return path_to_IAM_OnDB_dataset diff --git a/tests/test_IAM_OnDB_Dataset.py b/tests/test_IAM_OnDB_Dataset.py new file mode 100644 index 0000000000000000000000000000000000000000..384bfb2c38ba367ddbd382e28e90260de2cff9df --- /dev/null +++ b/tests/test_IAM_OnDB_Dataset.py @@ -0,0 +1,80 @@ +from pathlib import Path + +import numpy as np +import pytest + +from xournalpp_htr.training.data.datasets import IAM_OnDB_Dataset + +# TODO: Maybe add check to check for certain samples in IAM_OnDB using the name of a sample? + + +@pytest.mark.data +def test_dataset_path_exists(get_path_to_IAM_OnDB_dataset: Path) -> None: + assert get_path_to_IAM_OnDB_dataset.exists() + + +@pytest.mark.data +def test_construction_with_limit(get_path_to_IAM_OnDB_dataset: Path) -> None: + limit = 5 + + ds = IAM_OnDB_Dataset( + path=get_path_to_IAM_OnDB_dataset, transform=None, limit=limit + ) + + assert len(ds) == limit + + +@pytest.mark.data +@pytest.mark.slow +def test_construction_no_limit(get_path_to_IAM_OnDB_dataset: Path) -> None: + ds = IAM_OnDB_Dataset(path=get_path_to_IAM_OnDB_dataset, transform=None, limit=-1) + + assert len(ds) == IAM_OnDB_Dataset.LENGTH + + +@pytest.mark.data +@pytest.mark.slow +def test_construction_no_limit_skip_carbune2020_fails( + get_path_to_IAM_OnDB_dataset: Path, +) -> None: + ds = IAM_OnDB_Dataset( + path=get_path_to_IAM_OnDB_dataset, + transform=None, + limit=-1, + skip_carbune2020_fails=True, + ) + + assert len(ds) == IAM_OnDB_Dataset.LENGTH - len( + IAM_OnDB_Dataset.SAMPLES_TO_SKIP_BC_CARBUNE2020_FAILS + ) + + +@pytest.mark.data +@pytest.mark.slow +def test_correctness_manually( + tmp_path: Path, get_path_to_IAM_OnDB_dataset: Path +) -> None: + # This saves samples to files so that one can inspect the correctness of the + # dataset manually. Enabling the pytest setting `-s` allows one to see where + # the files were saved temporarily. + + NR_SAMPLES = 100 + LIMIT = -1 + + print() + print(f'Samples saved at: "{tmp_path}"') + print() + + ds = IAM_OnDB_Dataset( + path=get_path_to_IAM_OnDB_dataset, transform=None, limit=LIMIT + ) + + # Get NR_SAMPLES reproducible random draws + rng = np.random.default_rng(1337) + index_list = np.arange(0, len(ds)) + rng.shuffle(index_list) + index_list = index_list[:NR_SAMPLES] + + for iam_index in index_list: + sample_name = ds[iam_index]["sample_name"] + ds.plot_sample_to_image_file(iam_index, tmp_path / Path(f"{sample_name}.png")) diff --git a/tests/test_PageDatasetFromOnline.py b/tests/test_PageDatasetFromOnline.py new file mode 100644 index 0000000000000000000000000000000000000000..928f0d3896dcf3fe218b5c5e986f607216185efb --- /dev/null +++ b/tests/test_PageDatasetFromOnline.py @@ -0,0 +1,162 @@ +from pathlib import Path + +import matplotlib.pyplot as plt +import pytest +import torch + +from xournalpp_htr.training.data.datasets import ( + IAM_OnDB_Dataset, + PageDatasetFromOnline, + PageDatasetFromOnlinePosition, +) + + +def test_compute(get_path_to_IAM_OnDB_dataset: Path): + limit = 100 + ds = IAM_OnDB_Dataset( + path=get_path_to_IAM_OnDB_dataset, transform=None, limit=limit + ) + + positions_height = 5.0 + positions = [ + PageDatasetFromOnlinePosition( + stroke_width=1, + page_index=0, + center_x=10.0, + center_y=10.0, + height=positions_height, + dataset_index=0, + ), + PageDatasetFromOnlinePosition( + stroke_width=1, + page_index=0, + center_x=10.0, + center_y=20.0, + height=positions_height, + dataset_index=1, + ), + ] + + print(len(ds.data)) + p_ds = PageDatasetFromOnline( + dataset=ds, + positions=positions, + page_size=(10, 10), + dpi=72, + ) + result = p_ds.compute() + p_ds.render_pages() + + for page_index in result: + plt.figure(figsize=(p_ds.page_size)) + for data in result[page_index]: + for stroke_nr in data["strokes"]: + x = data["strokes"][stroke_nr]["x"] + y = data["strokes"][stroke_nr]["y"] + # label = data["label"] + plt.plot( + x, + y, + c="black", + ) + plt.gca().set_aspect("equal") + plt.show() + plt.close() + + # TODO: construct segmentation mask as well + + +@pytest.mark.visual_check +def test_render_page_and_mask(get_path_to_IAM_OnDB_dataset: Path, tmp_path: Path): + print(f"\n\nPath to check out: {tmp_path}.\n") + + limit = 100 + ds = IAM_OnDB_Dataset( + path=get_path_to_IAM_OnDB_dataset, transform=None, limit=limit + ) + + positions_height = 15.0 + positions = [ + PageDatasetFromOnlinePosition( + stroke_width=1, + page_index=0, + center_x=105.0, + center_y=100.0, + height=positions_height, + dataset_index=0, + ), + PageDatasetFromOnlinePosition( + stroke_width=1, + page_index=0, + center_x=105.0, + center_y=200.0, + height=positions_height, + dataset_index=1, + ), + ] + + p_ds = PageDatasetFromOnline( + dataset=ds, + positions=positions, + page_size=(297, 210), + cache_dir=tmp_path, + dpi=72, + ) + for key in p_ds.data: + p_ds.render_page_and_mask( + page_index=key, + output_path_page=tmp_path / f"test_page_{key}.png", + output_path_mask=tmp_path / f"test_mask_{key}.png", + ) + + +@pytest.mark.visual_check +def test_getitem(get_path_to_IAM_OnDB_dataset: Path, tmp_path: Path): + print(f"\n\nPath to check out: {tmp_path}.\n") + + limit = 100 + ds = IAM_OnDB_Dataset( + path=get_path_to_IAM_OnDB_dataset, transform=None, limit=limit + ) + + positions_height = 15.0 + positions = [ + PageDatasetFromOnlinePosition( + stroke_width=1, + page_index=0, + center_x=105.0, + center_y=100.0, + height=positions_height, + dataset_index=0, + ), + PageDatasetFromOnlinePosition( + stroke_width=1, + page_index=0, + center_x=105.0, + center_y=200.0, + height=positions_height, + dataset_index=1, + ), + ] + + p_ds = PageDatasetFromOnline( + dataset=ds, + positions=positions, + page_size=(297, 210), + cache_dir=tmp_path, + dpi=72, + ) + sample = p_ds[0] + + image = sample["image"] + segmentation_mask = sample["segmentation_mask"] + + assert isinstance(image, torch.Tensor) + assert image.shape[0] == 3 + assert image.dtype == torch.uint8 + assert image.shape[1:] == segmentation_mask.shape[1:] + assert segmentation_mask.shape[0] == 3 + assert segmentation_mask.dtype == torch.uint8 + + +# TODO: Add tests for all `PageDatasetFromOnline` functions - really? diff --git a/tests/test_XournalDocument.py b/tests/test_XournalDocument.py new file mode 100644 index 0000000000000000000000000000000000000000..fb236026e69624677e0fb6238025a043e1c7a393 --- /dev/null +++ b/tests/test_XournalDocument.py @@ -0,0 +1,65 @@ +from pathlib import Path + +import pytest + +from xournalpp_htr.documents import XournalppDocument, get_document + + +def test_XournalppDocument(get_path_to_minimal_test_data: Path) -> None: + """Tests `XournalppDocument` and thereby its `load_data` function.""" + xpp_document = XournalppDocument(get_path_to_minimal_test_data) + assert xpp_document.path == get_path_to_minimal_test_data + assert len(xpp_document.pages) == 1 + assert xpp_document.DPI == 72 + page = xpp_document.pages[0] + assert page.meta_data["width"] == "612" + assert page.meta_data["height"] == "792" + assert page.background["type"] == "solid" + assert page.background["color"] == "#ffffffff" + assert page.background["style"] == "lined" + assert len(page.layers) == 1 + layer = page.layers[0] + assert len(layer.strokes) == 85 + + +def test_get_document_xopp(get_path_to_minimal_test_data: Path) -> None: + """Tests `get_document` function for a `xopp` file.""" + xpp_document = XournalppDocument(get_path_to_minimal_test_data) + document = get_document(get_path_to_minimal_test_data) + assert xpp_document.path == document.path == get_path_to_minimal_test_data + assert len(xpp_document.pages) == len(document.pages) == 1 + assert xpp_document.DPI == document.DPI + assert xpp_document.pages[0].meta_data == document.pages[0].meta_data + assert xpp_document.pages[0].background == document.pages[0].background + assert len(xpp_document.pages[0].layers) == len(document.pages[0].layers) == 1 + assert ( + len(xpp_document.pages[0].layers[0].strokes) + == len(document.pages[0].layers[0].strokes) + == 85 + ) + + +def test_get_document_unsupported() -> None: + """Tests `get_document` function for an unsupported file extension.""" + with pytest.raises(NotImplementedError): + get_document(Path("/a/file/that/is.unsupported")) + + +def test_get_min_max_coordintes_per_page(get_path_to_minimal_test_data: Path) -> None: + """ + Regression test of `Document.get_min_max_coordintes_per_page` function. + """ + xpp_document = XournalppDocument(get_path_to_minimal_test_data) + + result = xpp_document.get_min_max_coordintes_per_page() + + expected_result = { + 0: { + "min_x": 110.41164, + "min_y": 127.30273, + "max_x": 573.78978, + "max_y": 516.39615, + } + } + + assert result == expected_result diff --git a/tests/test_installation.py b/tests/test_installation.py new file mode 100644 index 0000000000000000000000000000000000000000..ca49489ffc66ed75aa043275eaeef29e625064c5 --- /dev/null +++ b/tests/test_installation.py @@ -0,0 +1,16 @@ +# ruff: noqa: F401 + +import pytest + + +@pytest.mark.installation +def test_htr_pipeline_package_installation() -> None: + """Test if code from `htr_pipeline` package can be imported.""" + from htr_pipeline import DetectorConfig, LineClusteringConfig, read_page + + +@pytest.mark.installation +def test_xournalpp_htr_package_installation() -> None: + """Test if code from `xournalpp_htr` package can be imported.""" + from xournalpp_htr.documents import XournalDocument, XournalppDocument + from xournalpp_htr.utils import export_to_pdf_with_xournalpp diff --git a/tests/test_run_htr.py b/tests/test_run_htr.py new file mode 100644 index 0000000000000000000000000000000000000000..5c80d754dc3432aba5d6cece2cb9fbe896b7e3b1 --- /dev/null +++ b/tests/test_run_htr.py @@ -0,0 +1,111 @@ +import urllib.request +from pathlib import Path + +import pytest + +from xournalpp_htr.pipeline import export_xournalpp_to_pdf_with_htr +from xournalpp_htr.run_htr import parse_arguments + + +@pytest.fixture +def get_repo_root_directory(request: pytest.FixtureRequest) -> Path: + """ + A `pytest` fixture that returns the root directory of the repository. + + This fixture retrieves the root directory from the pytest configuration + and asserts that the directory contains a "README.md" file. It is based + on an example provided in a + [Stack Overflow answer](https://stackoverflow.com/a/57039134). + + :param request: A `pytest` fixture that provides information about the + requesting test function. + :returns: A Path object representing the root directory of the repository. + :raises AssertionError: If the "README.md" file is not found in the root + directory. + """ + rootdir = Path(request.config.rootdir) + assert (rootdir / "README.md").is_file() + return rootdir + + +@pytest.fixture +def get_path_to_minimal_test_data(get_repo_root_directory: Path) -> Path: + """Fixture to retrieve the path to the minimal test data file. + + This function checks for the existence of a specific test data file + located at `${repo_root}/tests/data/2024-07-26_minimal.xopp`. If the + file does not exist, it is downloaded from a predefined URL and saved + at the specified location so that it can be retrieved directly next + time; i.e. it is cached. + + :param get_repo_root_directory: A fixture that provides the root + directory of the repository. + :returns: The path to the minimal test data file. + """ + + path_to_minimal_test_data = ( + get_repo_root_directory / "tests/data/2024-07-26_minimal.xopp" + ) + + if not path_to_minimal_test_data.is_file(): + url = "https://bit.ly/2024-07-26_minimal_xopp" + urllib.request.urlretrieve(url, path_to_minimal_test_data) + + return path_to_minimal_test_data + + +@pytest.mark.installation +def test_parse_arguments_empty() -> None: + """Test `parse_arguments` with no command-line arguments. + + Ensures that `parse_arguments` raises a `SystemExit` exception when + called without arguments. This typically occurs when required + arguments are missing, triggering `sys.exit()`. + + Marked with `installation` for selective test runs. + + :returns: None + """ + with pytest.raises(SystemExit): + parse_arguments() + + +@pytest.mark.installation +def test_parse_arguments_full() -> None: + """ + Test the `parse_arguments` function with a full set of input arguments. + + This test verifies that the `parse_arguments` function correctly + parses a complete set of command-line arguments. + """ + args = parse_arguments("-if input -of output -m dummy -pid dir -sp") + assert len(args) == 5 + assert args["input_file"].stem == "input" + assert args["output_file"].stem == "output" + assert args["model"] == "dummy" + assert args["prediction_image_dir"].stem == "dir" + assert args["show_predictions"] + + +def test_main(get_path_to_minimal_test_data: Path, tmp_path: Path) -> None: + """Tests the `main` function using minimal test data. + + Note: This function is currently not executed in Github Actions due to + the requirement of a specific `Xournal++` version for the + `export_to_pdf_with_xournalpp` function. + + :param get_path_to_minimal_test_data: Fixture to obtain path to the + minimal test data file. + :param tmp_path: Temporary path fixture used for storing the output + PDF file as a temporary file. + """ + + args = { + "input_file": get_path_to_minimal_test_data, + "output_file": tmp_path / Path("test_main.pdf"), + "model": "2024-07-18_htr_pipeline", # TODO: Add a `dummy` to test pipeline w/o ML part + "prediction_image_dir": None, + "show_predictions": False, + } + + export_xournalpp_to_pdf_with_htr(args) diff --git a/xournalpp_htr/__init__.py b/xournalpp_htr/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..8b137891791fe96927ad78e64b0aad7bded08bdc --- /dev/null +++ b/xournalpp_htr/__init__.py @@ -0,0 +1 @@ + diff --git a/xournalpp_htr/demo_concept_1.py b/xournalpp_htr/demo_concept_1.py new file mode 100644 index 0000000000000000000000000000000000000000..b28af3937df86589a6b5e4d5d70380a299dedec6 --- /dev/null +++ b/xournalpp_htr/demo_concept_1.py @@ -0,0 +1,121 @@ +import argparse +from pathlib import Path + +import cv2 +import matplotlib.pyplot as plt +from htr_pipeline import DetectorConfig, LineClusteringConfig, read_page + +from xournalpp_htr.documents import XournalDocument + + +def parse_arguments(): + """ + Parse arguments from command line. + """ + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "-if", + "--input-file", + type=lambda p: Path(p).absolute(), + required=True, + help="Path to the input file.", + ) + args = vars(parser.parse_args()) + return args + + +def main(args): + TMP_FILE = Path.home() / Path("xournalpp_htr_tmp_image.jpg") + + file_ending = args["input_file"].suffix + + if file_ending == ".xoj": + document = XournalDocument(args["input_file"]) + else: + raise NotImplementedError( + f'File ending "{file_ending}" currently not readable.' + ) + + nr_pages = len(document.pages) + + for page_index in range(nr_pages): + page_str = f"Page {page_index+1} / {nr_pages}" + print() + print("=" * len(page_str)) + print(page_str) + print("=" * len(page_str)) + + print() + print("I recognised:") + print() + + # ============== + # Write document + # ============== + + written_file = document.save_page_as_image(page_index, TMP_FILE, False, dpi=150) + + # ====== + # Do HTR + # ====== + + # read image + img = cv2.imread(str(written_file), cv2.IMREAD_GRAYSCALE) + + # detect and read text + # height = 700 # good + # enlarge = 5 + # enlarge = 10 + # height = 1000 # good + # height = 1600 # not good + scale = 0.4 + margin = 5 + read_lines = read_page( + img, + DetectorConfig(scale=scale, margin=margin), + line_clustering_config=LineClusteringConfig(min_words_per_line=2), + ) + + # To prepare plotting + img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) + + # output text + for read_line in read_lines: + recognitions = " ".join(read_word.text for read_word in read_line) + print(f'- "{recognitions}"') + for read_word in read_line: + box_int = read_word.aabb.enlarge_to_int_grid() + + img = cv2.rectangle( + img, + (int(box_int.xmin), int(box_int.ymax)), + (int(box_int.xmax), int(box_int.ymin)), + (255, 0, 0), + 2, + ) + + img = cv2.putText( + img, + text=read_word.text, + org=(int(box_int.xmin), int(box_int.ymin)), + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=1, + color=(255, 0, 0), + thickness=1, + ) + + plt_image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + plt.imshow(plt_image) + plt.title(page_str) + plt.show() + + TMP_FILE.unlink(missing_ok=True) + + # TODO: Export as PDF? Try to write a script that uses XOJ as input and exports a PDF with text layer + + +if __name__ == "__main__": + args = parse_arguments() + main(args) diff --git a/xournalpp_htr/documents.py b/xournalpp_htr/documents.py new file mode 100644 index 0000000000000000000000000000000000000000..b17102b67d8541e6d1cf375c7716eb623c995675 --- /dev/null +++ b/xournalpp_htr/documents.py @@ -0,0 +1,228 @@ +import gzip +from abc import ABC, abstractmethod +from dataclasses import dataclass +from pathlib import Path +from typing import Dict + +import matplotlib.pyplot as plt +import numpy as np +from bs4 import BeautifulSoup as bs + + +@dataclass +class Page: + """Class for keeping track of document page.""" + + meta_data: dict + background: dict + layers: list + + +@dataclass +class Layer: + """Class for keeping track of document page layer.""" + + strokes: list + + +@dataclass +class Stroke: + """Class for keeping track of strokes.""" + + x: np.array + y: np.array + meta_data: dict + + +class Document(ABC): + def __init__(self, path: Path): + self.path = path + self.pages = [] + self.DPI = -1 + self.load_data() + + @abstractmethod + def load_data(self): + """ + Loads data of document. + + Data comprises of stroke data on layers and pages as well as DPI. + """ + pass + + def save_page_as_image( + self, + page_index: int, + out_path: Path, + black_white: bool = False, + dpi: float = 72.0, + ) -> Path: + """ + Save document page as image. + + #TODO: I am using `matplotlib` here. Alternatively, OpenCV could do the trick as well. + + :param page_index: Index of page to save. + :param output: Output path. Its file type determines output file type. + :param black_white: Save image as black/white image if True. + :param dpi: DPI of exported image. + :returns: Output path. + """ + p = self.pages[page_index] + + fig_width_inch = float(p.meta_data["width"]) / self.DPI + fig_height_inch = float(p.meta_data["height"]) / self.DPI + + plt.figure(figsize=(fig_width_inch, fig_height_inch), dpi=self.DPI) + for layer in p.layers: + for stroke in layer.strokes: + c = "black" if black_white else stroke.meta_data["color"] + plt.plot(stroke.x / self.DPI, -stroke.y / self.DPI, c=c) + plt.xlim(0, fig_width_inch) + plt.ylim(-fig_height_inch, 0) + plt.axis("off") + plt.subplots_adjust(bottom=0, top=1, left=0, right=1) + plt.savefig(out_path, dpi=dpi) + plt.close() + + return out_path + + def get_min_max_coordinates_per_page(self) -> Dict[int, Dict[str, float]]: + """ + Compute the minimum and maximum x and y coordinate values for each page. + + This method iterates over all pages, and for each page, computes the minimum + and maximum x and y values from all strokes present in each layer of the page. + The results are stored in a dictionary, with the page index as the key and another + dictionary containing min and max values as the value. + + :returns: A dictionary where each key is the index of a page (int) and the + corresponding value is a dictionary containing the min and max + x and y values as follows: + { + "min_x": float, # Minimum x value for the page + "min_y": float, # Minimum y value for the page + "max_x": float, # Maximum x value for the page + "max_y": float # Maximum y value for the page + } + :rtype: Dict[int, Dict[str, float]] + """ + result: Dict[int, Dict[str, float]] = {} + for i_page, page in enumerate(self.pages): + min_x: float = np.inf + min_y: float = np.inf + max_x: float = -np.inf + max_y: float = -np.inf + for layer in page.layers: + for stroke in layer.strokes: + if stroke.x.max() > max_x: + max_x = stroke.x.max() + if stroke.y.max() > max_y: + max_y = stroke.y.max() + if stroke.x.min() < min_x: + min_x = stroke.x.min() + if stroke.y.min() < min_y: + min_y = stroke.y.min() + result[i_page] = { + "min_x": min_x, + "min_y": min_y, + "max_x": max_x, + "max_y": max_y, + } + return result + + # TODO: Add method to get all strokes on a single page in a list. I use this 2x in `annotate.py`! + + +class XournalDocument(Document): + def load_data(self): + """Load Xournal document content.""" + + with gzip.open(self.path, "r") as f: + content = f.read().decode("utf-8") + + bs_content = bs(content, "lxml") + + for page in bs_content.find_all("page"): + layers = [] + for layer in page.find_all("layer"): + strokes = [] + for stroke in layer.find_all("stroke"): + x, y = np.fromstring(stroke.text, sep=" ").reshape(-1, 2).T + s = Stroke(x, y, stroke.attrs) + strokes.append(s) + + layers.append(Layer(strokes)) + + background = page.find_all("background") + assert len(background) == 1 + background = background[0].attrs + + p = Page(page.attrs, background, layers) + + self.pages.append(p) + + self.DPI = 72 + + +class XournalppDocument(Document): + def load_data(self): + """Load Xournal document content.""" + + with gzip.open(self.path, "r") as f: + content = f.read().decode("utf-8") + + bs_content = bs(content, "lxml") + + for page in bs_content.find_all("page"): + layers = [] + for layer in page.find_all("layer"): + strokes = [] + for stroke in layer.find_all("stroke"): + x, y = np.fromstring(stroke.text, sep=" ").reshape(-1, 2).T + s = Stroke(x, y, stroke.attrs) + strokes.append(s) + + layers.append(Layer(strokes)) + + background = page.find_all("background") + assert len(background) == 1 + background = background[0].attrs + + p = Page(page.attrs, background, layers) + + self.pages.append(p) + + self.DPI = 72 + + +def get_document(path: Path) -> Document: + """ + Loads a document from a given file path based on the file extension. + + This function determines the appropriate document type to load by + examining the file extension of the provided path. It supports files + with the extensions `.xoj` and `.xopp`, returning a corresponding + document object. If the file extension is not recognized, a + `NotImplementedError` is raised. + + :param path: The file path to the document. + :returns: An instance of the appropriate `Document` class. Either + `XournalDocument` or `XournalppDocument`. + :raises NotImplementedError: If the file extension is not supported. + Supported file extensions are `.xoj` and + `.xopp`. + """ + + file_ending = path.suffix + + if file_ending == ".xoj": + document = XournalDocument(path) + elif file_ending == ".xopp": + document = XournalppDocument(path) + else: + raise NotImplementedError( + f'File ending "{file_ending}" currently not readable.' + ) + + return document diff --git a/xournalpp_htr/models.py b/xournalpp_htr/models.py new file mode 100644 index 0000000000000000000000000000000000000000..cb80a91ed602d1304142b08d5a26dfe8bcd7134f --- /dev/null +++ b/xournalpp_htr/models.py @@ -0,0 +1,128 @@ +# Add code related to models here. Mostly for inference as there will exist +# another module for training or loading custom models. + +import tempfile +from pathlib import Path + +import cv2 +import matplotlib.pyplot as plt +from htr_pipeline import DetectorConfig, LineClusteringConfig, read_page +from tqdm import tqdm + + +def compute_predictions(model_name: str, document) -> dict: + predictions = {} + + if model_name == "2024-07-18_htr_pipeline": + nr_pages = len(document.pages) + + for page_index in tqdm(range(nr_pages), desc="Recognition"): + with tempfile.NamedTemporaryFile( + dir="/tmp", + delete=False, + prefix=f"xournalpp_htr__page{page_index}__", + suffix=".jpg", + ) as tmpfile: + TMP_FILE = Path(tmpfile.name) + + written_file = document.save_page_as_image( + page_index, TMP_FILE, False, dpi=150 + ) + + # ====== + # Do HTR + # ====== + + # read image + img = cv2.imread(str(written_file), cv2.IMREAD_GRAYSCALE) + + # detect and read text + # height = 700 # good + # enlarge = 5 + # enlarge = 10 + # height = 1000 # good + # height = 1600 # not good + scale = 0.4 + margin = 5 + read_lines = read_page( + img, + DetectorConfig(scale=scale, margin=margin), + line_clustering_config=LineClusteringConfig(min_words_per_line=2), + ) + + predictions_page = [] + for line in read_lines: + for word in line: + data = { + "page_index": page_index, + "text": word.text, + "xmin": word.aabb.xmin, + "xmax": word.aabb.xmax, + "ymin": word.aabb.ymin, + "ymax": word.aabb.ymax, + } + predictions_page.append(data) + predictions[page_index] = predictions_page + + else: + raise NotImplementedError(f'Model "{model_name}" not implemented.') + + return predictions + + +def store_predictions_as_images( + output_directory: Path, predictions: dict, document +) -> None: + output_directory.mkdir(parents=True, exist_ok=True) + + nr_pages = len(document.pages) + + for page_index in tqdm(range(nr_pages), desc="Store predictions as images"): + file_name = output_directory / f"page{page_index}.jpg" + file_name_ocrd = output_directory / f"page{page_index}_ocrd.jpg" + + written_file = document.save_page_as_image( + page_index, file_name, False, dpi=150 + ) + + # ====== + # Do HTR + # ====== + + # read image + img = cv2.imread(str(written_file), cv2.IMREAD_GRAYSCALE) + + # To prepare plotting + img = cv2.cvtColor(img, cv2.COLOR_GRAY2RGB) + + # Impose predictions on image + for prediction in predictions[page_index]: + text = prediction["text"] + xmin = prediction["xmin"] + xmax = prediction["xmax"] + ymin = prediction["ymin"] + ymax = prediction["ymax"] + + img = cv2.rectangle( + img, (int(xmin), int(ymax)), (int(xmax), int(ymin)), (255, 0, 0), 2 + ) + + img = cv2.putText( + img, + text=text, + org=(int(xmin), int(ymin)), + fontFace=cv2.FONT_HERSHEY_SIMPLEX, + fontScale=1, + color=(255, 0, 0), + thickness=1, + ) + + plt_image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + figure_aspect_ratio = float( + document.pages[page_index].meta_data["height"] + ) / float(document.pages[page_index].meta_data["width"]) + plt.figure(figsize=(10, 10 * figure_aspect_ratio)) + plt.imshow(plt_image) + plt.savefig(file_name_ocrd, dpi=150) + plt.close() diff --git a/xournalpp_htr/pipeline.py b/xournalpp_htr/pipeline.py new file mode 100644 index 0000000000000000000000000000000000000000..a63c95da7ce2fc42f5a41c3ff99be509a65f00af --- /dev/null +++ b/xournalpp_htr/pipeline.py @@ -0,0 +1,62 @@ +from xournalpp_htr.documents import get_document +from xournalpp_htr.models import compute_predictions, store_predictions_as_images +from xournalpp_htr.utils import export_to_pdf_with_xournalpp +from xournalpp_htr.xio import get_temporary_filename, write_predictions_to_PDF + + +def export_xournalpp_to_pdf_with_htr(args: dict) -> None: + """Main function that performs HTR. + + This function exports an Xournal(++) file to a PDF file, performs Handwritten Text Recognition (HTR) on the file using + the specified model, and stores the predictions as hidden text in the resulting PDF file. The function can also plot + the predictions to ensure they are working correctly and save them to a directory for later inspection. + + This function can be imported elsewhere to use, e.g., in Jupyter notebooks or tests. + + :param args: Dictionary containing the following input parameters: `input_file' (Path; path to the input Xournal(++) file), + 'prediction_image_dir' (Path or None; directory for storing prediction images (optional)), 'output_file' + (Path; path to the output PDF file), 'model' (str; name of the HTR model to use for predictions) and + `show_predictions` (bool; switch to render visible prediction texts in PDF instead of invisible texts). + :returns: None + """ + + # Goal + # + # Export as PDF: Write a script that uses XOJ as input and exports a PDF with text layer. + + # Settings + + input_file = args["input_file"] + prediction_image_dir = args["prediction_image_dir"] + output_file = args["output_file"] + debug_htr = args["show_predictions"] + model = args["model"] + + output_file_tmp_noOCR = get_temporary_filename() + + # Step 1: XOJ to PDF + # + # First, turn test file from `xoj` into `pdf`. + + export_to_pdf_with_xournalpp(input_file, output_file_tmp_noOCR) + + # Step 2: Perform HTR predictions + + document = get_document(input_file) + + predictions = compute_predictions(model_name=model, document=document) + + # Plot the predictions to ensure that they are working correctly: + + if prediction_image_dir: + store_predictions_as_images(prediction_image_dir, predictions, document) + + # Step 3: Store predictions in PDF + write_predictions_to_PDF( + output_file_tmp_noOCR, + output_file, + predictions, + debug_htr, + ) + + print("xournalpp_htr: Done!") diff --git a/xournalpp_htr/run_htr.py b/xournalpp_htr/run_htr.py new file mode 100644 index 0000000000000000000000000000000000000000..25479e723bef61d8246356473cdee967314f79dc --- /dev/null +++ b/xournalpp_htr/run_htr.py @@ -0,0 +1,8 @@ +"""Script to perform HTR on Xournal(++) document.""" + +from xournalpp_htr.pipeline import export_xournalpp_to_pdf_with_htr +from xournalpp_htr.utils import parse_arguments + +if __name__ == "__main__": + args = parse_arguments() + export_xournalpp_to_pdf_with_htr(args) diff --git a/xournalpp_htr/training/WordDetectorNN/.env.example b/xournalpp_htr/training/WordDetectorNN/.env.example new file mode 100644 index 0000000000000000000000000000000000000000..e5a5d10186dfcc68f798ccafd7045cf4502ae04a --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/.env.example @@ -0,0 +1,6 @@ +DEMO=1 +SB_URL= +SB_KEY= +SB_BUCKET_NAME= +SB_SCHEMA_NAME= +SB_TABLE_NAME= diff --git a/xournalpp_htr/training/WordDetectorNN/.python-version b/xournalpp_htr/training/WordDetectorNN/.python-version new file mode 100644 index 0000000000000000000000000000000000000000..e4fba2183587225f216eeada4c78dfab6b2e65f5 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/.python-version @@ -0,0 +1 @@ +3.12 diff --git a/xournalpp_htr/training/WordDetectorNN/README.md b/xournalpp_htr/training/WordDetectorNN/README.md new file mode 100644 index 0000000000000000000000000000000000000000..2ea4621768e3ca140a18ac362246959151325042 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/README.md @@ -0,0 +1,110 @@ +--- +title: Xournal++ HTR WordDetectorNN +emoji: 📄 +colorFrom: purple +colorTo: indigo +sdk: gradio +sdk_version: 5.44.1 +app_file: demo.py +pinned: false +--- + +# Training WordDetectorNN + +[🤗 demo](https://huggingface.co/spaces/PellelNitram/xournalpp_htr_WordDetectorNN) + +This subfolder contains the standalone training code and resources for training the WordDetectorNN model. +The [WordDetectorNN](https://github.com/githubharald/WordDetectorNN) model was originally created by +[Harald Scheidl](https://github.com/githubharald/WordDetectorNN) & this work here just reimplements it +with some best practises to later integrate it into a Xournal++ HTR pipeline. + +## Project Structure + +This subfolder operates as an independent module within the main Xournal++ HTR repository. It has been designed to be completely self-contained to enable: + +- Simple experimentation without complex dependency management +- Rapid prototyping and iteration +- Isolated development without affecting the main repository + +### Why Independent? + +The decision to keep this as a standalone subfolder (rather than integrating it directly into the main repository) allows for: + +- Streamlined experimentation +- Avoiding dependency conflicts with the main project +- Faster development cycles +- Cleaner separation of concerns during the research phase + +## Installation + +*(TODO: Check that this is correct next time I train.)* + +1. `uv init --no-workspace` +2. `uv venv` +3. `uv sync`; by the way, here is a [useful tutorial](https://docs.astral.sh/uv/guides/integration/pytorch/#installing-pytorch) on how to install pytorch w/ uv. + +## Current Status + +Everything from the original WordDetectorNN model has been reimplemented except for training data augmentations. +It is a reasonable idea to implement these training data augmentations in the future. After this is done, the +betham sample should be checked again for correctness as this example fails horrible currently. + +This folder remains independent from the main code base until WordDetectorNN becomes part of a pipeline and, before +that, Xournal++ HTR can be installed both for users and developers properly. + +## Future Integration + +After successful model training, integration work will include: + +- Dependency alignment with the main project structure +- Integrate training & inference code into main code base so that it is usable as pipeline + +## Deployment as Hugging Face Space + +The model implemented and trained here is currently deployed as HF Gradio Space [here](https://huggingface.co/spaces/PellelNitram/xournalpp_htr_WordDetectorNN). + +The deployment to there is currently done as manual process. The files are copied to the Space manually. + +In the future, it is worth to use a Docker HF space to gain +finer grained control about the deployment process and to automate it. + +## SupaBase database commands + +Create the events table: + +```sql +create table word_detector_net.hf_space_events ( + id bigserial primary key, + timestamp timestamptz not null, + demo boolean not null, + uuid text not null, + donate_data bool not null, + contains_image bool not null +); +``` + +Create bucket: + +``` +WordDetectorNN_hf_space_images +``` + +Configure table: + +``` +-- Schema access +grant usage on schema word_detector_net to service_role; + +-- Table access +grant insert, select on table word_detector_net.hf_space_events to service_role; + +-- Sequence access for autoincrement IDs +grant usage, select, update on sequence word_detector_net.hf_space_events_id_seq to service_role; +``` + +## Outlook + +Considerations for when the model is integrated into a pipeline: + +- Train using data augmentations. +- Use PIL images everywhere instead of numpy to keep track of channel order. \ No newline at end of file diff --git a/xournalpp_htr/training/WordDetectorNN/Untitled-2025-08-30-1038-gradio-demo-architecture.excalidraw b/xournalpp_htr/training/WordDetectorNN/Untitled-2025-08-30-1038-gradio-demo-architecture.excalidraw new file mode 100644 index 0000000000000000000000000000000000000000..e6dedce2286fceb64d3ddcfe24b7b49d13200e2e --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/Untitled-2025-08-30-1038-gradio-demo-architecture.excalidraw @@ -0,0 +1,541 @@ +{ + "type": "excalidraw", + "version": 2, + "source": "https://excalidraw.com", + "elements": [ + { + "id": "uh58LpNBRrHVnh5z9J33a", + "type": "rectangle", + "x": 831, + "y": 252, + "width": 207, + "height": 313, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a0", + "roundness": { + "type": 3 + }, + "seed": 1402592849, + "version": 150, + "versionNonce": 175090751, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "1-qpLVAiR8gA4rey2JAmK" + }, + { + "id": "lW-sEEZDBTVGNogelba8j", + "type": "arrow" + }, + { + "id": "3MImhzI5bcvhLXp2NOkXh", + "type": "arrow" + } + ], + "updated": 1756543253357, + "link": null, + "locked": false + }, + { + "id": "1-qpLVAiR8gA4rey2JAmK", + "type": "text", + "x": 881.2416648864746, + "y": 396, + "width": 106.51667022705078, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a1", + "roundness": null, + "seed": 1349176671, + "version": 128, + "versionNonce": 625855583, + "isDeleted": false, + "boundElements": null, + "updated": 1756543253357, + "link": null, + "locked": false, + "text": "Gradio App", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "uh58LpNBRrHVnh5z9J33a", + "originalText": "Gradio App", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "1JhpqqS9cLSNQWz521noc", + "type": "rectangle", + "x": 728, + "y": 213, + "width": 391, + "height": 382, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a2", + "roundness": { + "type": 3 + }, + "seed": 205237023, + "version": 35, + "versionNonce": 1091422833, + "isDeleted": false, + "boundElements": [ + { + "id": "3MImhzI5bcvhLXp2NOkXh", + "type": "arrow" + } + ], + "updated": 1756543216339, + "link": null, + "locked": false + }, + { + "id": "2T1y15h530ZFUEvHH0Zws", + "type": "text", + "x": 795, + "y": 178, + "width": 260.8999938964844, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a3", + "roundness": null, + "seed": 341954033, + "version": 77, + "versionNonce": 1895825265, + "isDeleted": false, + "boundElements": null, + "updated": 1756543234318, + "link": null, + "locked": false, + "text": "Hosted on HF w/ free CPU", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "left", + "verticalAlign": "top", + "containerId": null, + "originalText": "Hosted on HF w/ free CPU", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "irbbUbP8XWEtyyEpPks-C", + "type": "rectangle", + "x": 1462.5, + "y": 358, + "width": 207, + "height": 136, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a4", + "roundness": { + "type": 3 + }, + "seed": 1468742641, + "version": 171, + "versionNonce": 2020488145, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "AZe2dSGaDZtQWnQ_n8zff" + }, + { + "id": "lW-sEEZDBTVGNogelba8j", + "type": "arrow" + } + ], + "updated": 1756543198113, + "link": null, + "locked": false + }, + { + "id": "AZe2dSGaDZtQWnQ_n8zff", + "type": "text", + "x": 1497.6333312988281, + "y": 401, + "width": 136.73333740234375, + "height": 50, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a5", + "roundness": null, + "seed": 394302929, + "version": 174, + "versionNonce": 837483249, + "isDeleted": false, + "boundElements": [], + "updated": 1756543361141, + "link": null, + "locked": false, + "text": "SupaBase\nStorage (=S3)", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "irbbUbP8XWEtyyEpPks-C", + "originalText": "SupaBase\nStorage (=S3)", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "lW-sEEZDBTVGNogelba8j", + "type": "arrow", + "x": 1045, + "y": 411.10244114114437, + "width": 407, + "height": 12.784095162900257, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a6", + "roundness": { + "type": 2 + }, + "seed": 1781940319, + "version": 140, + "versionNonce": 1174672575, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "6gVvzXl5ptYuNMFVcaZzy" + } + ], + "updated": 1756543253421, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 407, + 12.784095162900257 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "uh58LpNBRrHVnh5z9J33a", + "focus": -0.0052868013323952665, + "gap": 7 + }, + "endBinding": { + "elementId": "irbbUbP8XWEtyyEpPks-C", + "focus": -0.0205971335146526, + "gap": 10.5 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "6gVvzXl5ptYuNMFVcaZzy", + "type": "text", + "x": 1181.4833335876465, + "y": 393, + "width": 80.03333282470703, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a7", + "roundness": null, + "seed": 1422296991, + "version": 48, + "versionNonce": 2042801567, + "isDeleted": false, + "boundElements": null, + "updated": 1756543195345, + "link": null, + "locked": false, + "text": "store\ndonated\ndata", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "lW-sEEZDBTVGNogelba8j", + "originalText": "store\ndonated\ndata", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "_qP0xJ9RwpSEwMH7ChURQ", + "type": "rectangle", + "x": 310.5, + "y": 359, + "width": 207, + "height": 136, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a8", + "roundness": { + "type": 3 + }, + "seed": 1678807761, + "version": 139, + "versionNonce": 1125909169, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "RE6M4uqc7RU2KuxgkQAEz" + }, + { + "id": "3MImhzI5bcvhLXp2NOkXh", + "type": "arrow" + } + ], + "updated": 1756543216339, + "link": null, + "locked": false + }, + { + "id": "RE6M4uqc7RU2KuxgkQAEz", + "type": "text", + "x": 391.78333282470703, + "y": 414.5, + "width": 44.43333435058594, + "height": 25, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "a9", + "roundness": null, + "seed": 1526206641, + "version": 122, + "versionNonce": 1188013969, + "isDeleted": false, + "boundElements": [], + "updated": 1756543209265, + "link": null, + "locked": false, + "text": "User", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "_qP0xJ9RwpSEwMH7ChURQ", + "originalText": "User", + "autoResize": true, + "lineHeight": 1.25 + }, + { + "id": "3MImhzI5bcvhLXp2NOkXh", + "type": "arrow", + "x": 532, + "y": 429.8452034464923, + "width": 278.0000000000001, + "height": 4.478935707673543, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aA", + "roundness": { + "type": 2 + }, + "seed": 537245521, + "version": 98, + "versionNonce": 859907295, + "isDeleted": false, + "boundElements": null, + "updated": 1756543253422, + "link": null, + "locked": false, + "points": [ + [ + 0, + 0 + ], + [ + 278.0000000000001, + -4.478935707673543 + ] + ], + "lastCommittedPoint": null, + "startBinding": { + "elementId": "_qP0xJ9RwpSEwMH7ChURQ", + "focus": 0.06814679051943168, + "gap": 14.5 + }, + "endBinding": { + "elementId": "uh58LpNBRrHVnh5z9J33a", + "focus": -0.0943048796527689, + "gap": 21 + }, + "startArrowhead": null, + "endArrowhead": "arrow", + "elbowed": false + }, + { + "id": "cQyU1FGFeyKZBHnUaHuSA", + "type": "rectangle", + "x": 852.5, + "y": 439, + "width": 160, + "height": 104, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aC", + "roundness": { + "type": 3 + }, + "seed": 826047025, + "version": 285, + "versionNonce": 1400327711, + "isDeleted": false, + "boundElements": [ + { + "type": "text", + "id": "e8j75HBTcSJiW4UDGiW1a" + } + ], + "updated": 1756543333233, + "link": null, + "locked": false + }, + { + "id": "e8j75HBTcSJiW4UDGiW1a", + "type": "text", + "x": 873.4500007629395, + "y": 453.5, + "width": 118.0999984741211, + "height": 75, + "angle": 0, + "strokeColor": "#1e1e1e", + "backgroundColor": "transparent", + "fillStyle": "solid", + "strokeWidth": 2, + "strokeStyle": "solid", + "roughness": 1, + "opacity": 100, + "groupIds": [], + "frameId": null, + "index": "aD", + "roundness": null, + "seed": 485493777, + "version": 333, + "versionNonce": 348533311, + "isDeleted": false, + "boundElements": [], + "updated": 1756543333233, + "link": null, + "locked": false, + "text": "model folder\nw/ file and\ninformation", + "fontSize": 20, + "fontFamily": 5, + "textAlign": "center", + "verticalAlign": "middle", + "containerId": "cQyU1FGFeyKZBHnUaHuSA", + "originalText": "model folder\nw/ file and information", + "autoResize": true, + "lineHeight": 1.25 + } + ], + "appState": { + "gridSize": 20, + "gridStep": 5, + "gridModeEnabled": false, + "viewBackgroundColor": "#ffffff", + "lockedMultiSelections": {} + }, + "files": {} +} \ No newline at end of file diff --git a/xournalpp_htr/training/WordDetectorNN/demo.py b/xournalpp_htr/training/WordDetectorNN/demo.py new file mode 100644 index 0000000000000000000000000000000000000000..9a03ea748496ff13e1fef6c785f8542980ebdb03 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/demo.py @@ -0,0 +1,138 @@ +import argparse +import os +from datetime import datetime, timezone +from pathlib import Path +from uuid import uuid4 + +import cv2 +import gradio as gr +import numpy as np +import torch +from dotenv import load_dotenv +from my_code import ( + draw_bboxes_on_image, + get_example_list, + run_image_through_network, + save_event, +) + +load_dotenv() + +DEMO = os.getenv("DEMO") == "1" +SB_URL = os.getenv("SB_URL") +SB_KEY = os.getenv("SB_KEY") +SB_BUCKET_NAME = os.getenv("SB_BUCKET_NAME") +SB_SCHEMA_NAME = os.getenv("SB_SCHEMA_NAME") +SB_TABLE_NAME = os.getenv("SB_TABLE_NAME") + +parser = argparse.ArgumentParser( + description="Train a WordDetectorNet model.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, +) +parser.add_argument( + "--model_path", + type=Path, + default=Path("best_model.pth"), + help="Path to trained pth model.", +) +parser.add_argument( + "--device", + type=str, + choices=["cpu", "auto"], + default="cpu", + help='Selects the device. "auto" selects GPU if available.', +) +args = vars(parser.parse_args()) + +model_path = args["model_path"] +device_selection = args["device"] + +print(f"Used args: {args}") + + +def process_image( + image: np.ndarray, # Is (H, W, 3) uint8 RGB; return needs to be the same + margin: float, + donate_data: bool, +) -> np.ndarray: + margin = int(margin) + + image_BGR = cv2.cvtColor(image.copy(), cv2.COLOR_RGB2BGR) + + image_gray = cv2.cvtColor(image_BGR, cv2.COLOR_BGR2GRAY) + + if device_selection == "auto": + device = "cuda" if torch.cuda.is_available() else "cpu" + else: + device = "cpu" + + # Inference + result = run_image_through_network( + image_grayscale=image_gray, + model_path=model_path, + device=device, + ) + + # Post processing + scaling_factors = np.array(image_gray.shape) / np.array( + result["model_input_image"].shape + ) + bboxes_scaled = [ + aabb.scale(*scaling_factors[::-1]).enlarge(margin_x=margin, margin_y=margin) + for aabb in result["aabbs"] + ] + vis_scaled = draw_bboxes_on_image(image_BGR, bboxes_scaled, denormalise=False) + + save_event( + { + "timestamp": datetime.now(timezone.utc), + "demo": DEMO, + "donate_data": donate_data, + "uuid": uuid4(), + "image": image, + }, + SB_URL=SB_URL, + SB_KEY=SB_KEY, + SB_SCHEMA_NAME=SB_SCHEMA_NAME, + SB_TABLE_NAME=SB_TABLE_NAME, + SB_BUCKET_NAME=SB_BUCKET_NAME, + ) + + return cv2.cvtColor(vis_scaled, cv2.COLOR_BGR2RGB) + + +demo = gr.Interface( + fn=process_image, + inputs=[ + gr.Image(type="numpy", label="Input image."), + gr.Slider( + minimum=0, + maximum=100, + value=0, + step=1, + label="Margin", + ), + gr.Checkbox( + value=False, + label="Donate Data", + info="By checking this box, you agree to share your uploaded image to help improve our open-source models. Donated data will be open source and freely available as dataset.", + ), + ], + outputs=gr.Image( + type="numpy", label="Input image with detected words superimposed." + ), + title="WordDetectorNN: Handwritten Word Detection", + description="Detect handwritten words in images. Upload an image, adjust the margin slider, and see bounding boxes around detected words.", + article=""" + ### About this project + This demo is part of **[Xournal++ HTR](https://github.com/PellelNitram/xournalpp_htr)**, an open-source effort to bring handwritten text recognition to [Xournal++](https://github.com/xournalpp/xournalpp). + + The original WordDetectorNN model was created by **Harald Scheidl** in his [WordDetectorNN repository](https://github.com/githubharald/WordDetectorNN). This project re-implements it with PyTorch best practices. Thanks Harald for the great work and inspiration! + + Donated data will contribute to an open-source dataset for the community. Thank you for supporting open-source innovation! + """, + examples=get_example_list(), + cache_examples=True, +) + +demo.launch() diff --git a/xournalpp_htr/training/WordDetectorNN/my_code.py b/xournalpp_htr/training/WordDetectorNN/my_code.py new file mode 100644 index 0000000000000000000000000000000000000000..1c658dd7e56209f25a60b5008c7c3b7fd445c813 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/my_code.py @@ -0,0 +1,1091 @@ +import io +import json +import pickle +import urllib.request +import xml.etree.ElementTree as ET +from collections import defaultdict +from pathlib import Path +from typing import List, NamedTuple, Optional, Tuple, TypedDict + +import cv2 +import numpy as np +import torch +import torch.nn as nn +import torch.nn.functional as F +from git import Repo +from sklearn.cluster import DBSCAN +from supabase import Client, create_client +from torch.utils.data import Dataset +from torchvision.models.resnet import BasicBlock, ResNet +from tqdm import tqdm + +# TODO: how to add w and h in all type annotations and datatype definitions? + + +class ImageDimensions(NamedTuple): + height: int + width: int + + +class BoundingBox: + def __init__( + self, + x_min: float, + y_min: float, + x_max: float, + y_max: float, + label: Optional[str] = None, + ): + """ + Initialize a bounding box. + (x_min, y_min): top-left corner + (x_max, y_max): bottom-right corner + label: optional class label + """ + # self.x_min: float = float(x_min) + # self.y_min: float = float(y_min) + # self.x_max: float = float(x_max) + # self.y_max: float = float(y_max) + self.x_min = float(x_min) + self.y_min = float(y_min) + self.x_max = float(x_max) + self.y_max = float(y_max) + self.label: Optional[str] = label + + def translate(self, dx: float, dy: float) -> "BoundingBox": + """Translate the bounding box by (dx, dy).""" + bbox_translated = BoundingBox( + self.x_min + dx, + self.y_min + dy, + self.x_max + dx, + self.y_max + dy, + self.label, + ) + return bbox_translated + + def scale(self, sx: float, sy: float) -> "BoundingBox": + """Scale the bounding box by sx and sy.""" + bbox_scaled = BoundingBox( + self.x_min * sx, + self.y_min * sy, + self.x_max * sx, + self.y_max * sy, + self.label, + ) + return bbox_scaled + + def as_type(self, new_type) -> "BoundingBox": + # TODO: This invalidates the above `float` types. Needs new type def therefore. + return BoundingBox( + new_type(self.x_min), + new_type(self.y_min), + new_type(self.x_max), + new_type(self.y_max), + self.label, + ) + + def scale_around_center(self, sx, sy) -> "BoundingBox": + center_x = (self.x_min + self.x_max) / 2 + center_y = (self.y_min + self.y_max) / 2 + return BoundingBox( + x_min=center_x - sx * (center_x - self.x_min), + y_min=center_y - sy * (center_y - self.y_min), + x_max=center_x + sx * (self.x_max - center_x), + y_max=center_y + sy * (self.y_max - center_y), + label=self.label, + ) + + def clip(self, clip_aabb) -> "BoundingBox": + return BoundingBox( + x_min=min(max(self.x_min, clip_aabb.x_min), clip_aabb.x_max), + y_min=min(max(self.y_min, clip_aabb.y_min), clip_aabb.y_max), + x_max=max(min(self.x_max, clip_aabb.x_max), clip_aabb.x_min), + y_max=max(min(self.y_max, clip_aabb.y_max), clip_aabb.y_min), + label=self.label, + ) + + def area(self): + """Return the area of the bounding box.""" + return max(0.0, self.x_max - self.x_min) * max(0.0, self.y_max - self.y_min) + + def enlarge_to_int_grid(self) -> "BoundingBox": + return BoundingBox( + x_min=np.floor(self.x_min), + y_min=np.floor(self.y_min), + x_max=np.ceil(self.x_max), + y_max=np.ceil(self.y_max), + label=self.label, + ) + + def enlarge(self, margin_x: float, margin_y: float) -> "BoundingBox": + return BoundingBox( + x_min=self.x_min - margin_x, + x_max=self.x_max + margin_x, + y_min=self.y_min - margin_y, + y_max=self.y_max + margin_y, + ) + + # def intersect(self, other): + # """Return the intersection area with another bounding box.""" + # x_min = max(self.x_min, other.x_min) + # y_min = max(self.y_min, other.y_min) + # x_max = min(self.x_max, other.x_max) + # y_max = min(self.y_max, other.y_max) + # if x_min < x_max and y_min < y_max: + # return (x_max - x_min) * (y_max - y_min) + # return 0.0 + + # def iou(self, other): + # """Return the Intersection over Union (IoU) with another bounding box.""" + # inter = self.intersect(other) + # union = self.area() + other.area() - inter + # if union == 0: + # return 0.0 + # return inter / union + + def __repr__(self) -> str: + return f"BoundingBox(x_min={self.x_min}, y_min={self.y_min}, x_max={self.x_max}, y_max={self.y_max}, label={self.label})" + + +# TODO later: Replace print with logging. +# TODO: deal w/ height and width better & relate to x and y - do that once I know things are working + + +# TODO: Rename +class MapOrdering: + """order of the maps encoding the aabbs around the words""" + + SEG_WORD = 0 + SEG_SURROUNDING = 1 + SEG_BACKGROUND = 2 + GEO_TOP = 3 + GEO_BOTTOM = 4 + GEO_LEFT = 5 + GEO_RIGHT = 6 + NUM_MAPS = 7 + + +def encode(input_size: ImageDimensions, output_size: ImageDimensions, gt): + f = output_size.height / input_size.height + gt_map = np.zeros((MapOrdering.NUM_MAPS,) + output_size) + for aabb in gt: + aabb = aabb.scale(f, f) + + # segmentation map + aabb_clip = BoundingBox(0, 0, output_size.width - 1, output_size.height - 1) + + aabb_word = aabb.scale_around_center(0.5, 0.5).as_type(int).clip(aabb_clip) + aabb_sur = aabb.as_type(int).clip(aabb_clip) + # TODO: fix hack to get ints + gt_map[ + MapOrdering.SEG_SURROUNDING, + int(aabb_sur.y_min) : int(aabb_sur.y_max) + 1, + int(aabb_sur.x_min) : int(aabb_sur.x_max) + 1, + ] = 1 + gt_map[ + MapOrdering.SEG_SURROUNDING, + int(aabb_word.y_min) : int(aabb_word.y_max) + 1, + int(aabb_word.x_min) : int(aabb_word.x_max) + 1, + ] = 0 + gt_map[ + MapOrdering.SEG_WORD, + int(aabb_word.y_min) : int(aabb_word.y_max) + 1, + int(aabb_word.x_min) : int(aabb_word.x_max) + 1, + ] = 1 + + # geometry map TODO vectorize + for x in range(int(aabb_word.x_min), int(aabb_word.x_max) + 1): + for y in range(int(aabb_word.y_min), int(aabb_word.y_max) + 1): + gt_map[MapOrdering.GEO_TOP, y, x] = y - aabb.y_min + gt_map[MapOrdering.GEO_BOTTOM, y, x] = aabb.y_max - y + gt_map[MapOrdering.GEO_LEFT, y, x] = x - aabb.x_min + gt_map[MapOrdering.GEO_RIGHT, y, x] = aabb.x_max - x + + gt_map[MapOrdering.SEG_BACKGROUND] = np.clip( + 1 - gt_map[MapOrdering.SEG_WORD] - gt_map[MapOrdering.SEG_SURROUNDING], 0, 1 + ) + + return gt_map + + +def subsample(idx, max_num): + """restrict fg indices to a maximum number""" + f = len(idx[0]) / max_num + if f > 1: + a = np.asarray([idx[0][int(j * f)] for j in range(max_num)], np.int64) + b = np.asarray([idx[1][int(j * f)] for j in range(max_num)], np.int64) + idx = (a, b) + return idx + + +def fg_by_threshold(thres, max_num=None): + """all pixels above threshold are fg pixels, optionally limited to a maximum number""" + + def func(seg_map): + idx = np.where(seg_map > thres) + if max_num is not None: + idx = subsample(idx, max_num) + return idx + + return func + + +def fg_by_cc(thres, max_num): + """take a maximum number of pixels per connected component, but at least 3 (->DBSCAN minPts)""" + + def func(seg_map): + seg_mask = (seg_map > thres).astype(np.uint8) + num_labels, label_img = cv2.connectedComponents(seg_mask, connectivity=4) + max_num_per_cc = max( + max_num // (num_labels + 1), 3 + ) # at least 3 because of DBSCAN clustering + + all_idx = [np.empty(0, np.int64), np.empty(0, np.int64)] + for curr_label in range(1, num_labels): + curr_idx = np.where(label_img == curr_label) + curr_idx = subsample(curr_idx, max_num_per_cc) + all_idx[0] = np.append(all_idx[0], curr_idx[0]) + all_idx[1] = np.append(all_idx[1], curr_idx[1]) + return tuple(all_idx) + + return func + + +def decode(nn_prediction, scale=1.0, comp_fg=fg_by_threshold(0.5)) -> List[BoundingBox]: # noqa: B008 + idx = comp_fg(nn_prediction[MapOrdering.SEG_WORD]) + nn_prediction_masked = nn_prediction[..., idx[0], idx[1]] + bounding_boxes = [] + for yc, xc, pred in zip(idx[0], idx[1], nn_prediction_masked.T): + t = pred[MapOrdering.GEO_TOP] + b = pred[MapOrdering.GEO_BOTTOM] + l = pred[MapOrdering.GEO_LEFT] # noqa: E741 + r = pred[MapOrdering.GEO_RIGHT] + bbox = BoundingBox(x_min=xc - l, x_max=xc + r, y_min=yc - t, y_max=yc + b) + bounding_boxes.append(bbox.scale(scale, scale)) + return bounding_boxes + + +class IAM_Dataset_Element(TypedDict): + image: np.ndarray + bounding_boxes: List[BoundingBox] + filename: str + gt_encoded: np.ndarray + + +class IAM_Dataset(Dataset): + """ + Loads, pre-processes, and caches the IAM Handwriting Database. + + This class handles the entire data preparation pipeline. On the first run, it + processes all images and ground truth files, resizes them, and saves them + to a cache file for extremely fast loading on subsequent runs. + + Inherits from `torch.utils.data.Dataset`, making it fully compatible with + PyTorch's DataLoader. + """ + + # TODO: Open question: what is (x,y) direction and (w, h)? -> happy for now but need to investigate and make it more explicit for production model! heuristically, it seems like w -> x and h -> y. + + # TODO: Has potential to be reworked in one IAM_Ds and one based on top of that to transform to dataset used in this modeling approach and therefore DataLoader; can be done later once I know things work + + _GT_DIR_NAME = "gt" + _IMG_DIR_NAME = "img" + _IMG_EXT = ".png" + _GT_EXT = "*.xml" + + def __init__( + self, + root_dir: Path, + input_size: ImageDimensions, + output_size: ImageDimensions, + force_rebuild_cache: bool = False, + transform=None, + cache_path: Path = Path("dataset_cache.pickle"), + ): + """ + Initializes the dataset. Checks for a cache file first. If it doesn't + exist, it builds one. + + Args: + root_dir (Path): The root directory of the dataset, containing 'gt' and 'img' subdirectories. + input_size (Tuple[int, int]): The target (height, width) for the network input images. + loaded_img_scale (float): A factor to initially scale down images to reduce memory + usage during pre-processing. Default is 0.25. + """ + super().__init__() + self.root_dir = root_dir + self.input_size = input_size + self.output_size = output_size + self.input_width = input_size.width + self.input_height = input_size.height + self.output_width = output_size.width + self.output_height = output_size.height + self.transform = transform + + assert ( + self.output_width / self.input_width + == self.output_height / self.input_height + ), "Input and output need to have same aspect ratio" # Same aspect ratio + + self.img_cache: List[np.ndarray] = [] + self.gt_cache: List[List[BoundingBox]] = [] + self.filename_cache: List[str] = [] + + if cache_path.exists() and not force_rebuild_cache: + print(f"Loading cached data from {cache_path}...") + self._load_from_cache(cache_path) + else: + print(f"Cache not found. Building and caching data from {self.root_dir}...") + self._preprocess_and_cache(cache_path) + + def _load_from_cache(self, cache_path: Path): + """Loads pre-processed data from a pickle file.""" + with open(cache_path, "rb") as f: + self.img_cache, self.gt_cache, self.filename_cache = pickle.load(f) + + def _preprocess_and_cache(self, cache_path: Path): + """Finds, processes, and caches all data samples.""" + gt_dir = self.root_dir / self._GT_DIR_NAME + img_dir = self.root_dir / self._IMG_DIR_NAME + + fn_gts = sorted(gt_dir.glob(self._GT_EXT)) + print(f"Found {len(fn_gts)} ground truth files. Processing...") + + # TODO: Make this task parallel! + + for fn_gt in tqdm(fn_gts, desc="Preprocessing IAM Dataset"): + fn_img = img_dir / (fn_gt.stem + self._IMG_EXT) + if not fn_img.exists(): + continue + + # Load image and GT + img = cv2.imread(fn_img, cv2.IMREAD_GRAYSCALE) + gt = self._parse_gt(fn_gt) + + # Pre-processing pipeline + img, gt = self._crop_page_to_content(img, gt) + img, gt = self._adjust_to_input_size(img, gt) + + self.img_cache.append(img) + self.gt_cache.append(gt) + self.filename_cache.append(fn_gt.stem) + + print(f"Preprocessing complete. Saving cache to {cache_path}...") + with open(cache_path, "wb") as f: + pickle.dump([self.img_cache, self.gt_cache, self.filename_cache], f) + print("Cache saved successfully.") + + def _parse_gt(self, fn_gt: Path) -> List[BoundingBox]: + """Parses an XML ground truth file to get word bounding boxes.""" + tree = ET.parse(fn_gt) + root = tree.getroot() + aabbs = [] + + for line in root.findall("./handwritten-part/line"): + for word in line.findall("./word"): + x_min, x_max, y_min, y_max = float("inf"), 0, float("inf"), 0 + components = word.findall("./cmp") + if not components: + continue + + for cmp in components: + x = float(cmp.attrib["x"]) + y = float(cmp.attrib["y"]) + w = float(cmp.attrib["width"]) + h = float(cmp.attrib["height"]) + x_min = min(x_min, x) + x_max = max(x_max, x + w) + y_min = min(y_min, y) + y_max = max(y_max, y + h) + + text = word.attrib["text"] + + # Scale coordinates to match the initially scaled image + aabb = BoundingBox(x_min, y_min, x_max, y_max, text) + aabbs.append(aabb) + return aabbs + + def _crop_page_to_content( + self, img: np.ndarray, gt: List[BoundingBox] + ) -> Tuple[np.ndarray, List[BoundingBox]]: + """Crops the image to the bounding box containing all words.""" + x_min = min(aabb.x_min for aabb in gt) + x_max = max(aabb.x_max for aabb in gt) + y_min = min(aabb.y_min for aabb in gt) + y_max = max(aabb.y_max for aabb in gt) + + gt_crop = [aabb.translate(-x_min, -y_min) for aabb in gt] + img_crop = img[ + int(y_min) : int(y_max), int(x_min) : int(x_max) + ] # TODO: Round correctly as opposed to just int'ing + return img_crop, gt_crop + + def _adjust_to_input_size( + self, img: np.ndarray, gt: List[BoundingBox] + ) -> Tuple[np.ndarray, List[BoundingBox]]: + """Resizes the image and AABBs to the final network input size.""" + h, w = img.shape + # print(f'incoming image: {h=} {w=}') # TODO: Use logging here. + sx = self.input_width / w + sy = self.input_height / h + # print(f'scaling factors: {sx=} {sy=}') # TODO: Use logging here. + gt_resized = [aabb.scale(sx, sy) for aabb in gt] + img_resized = cv2.resize( + img, dsize=(self.input_width, self.input_height) + ) # cv2 uses (w, h) + return img_resized, gt_resized + + def __len__(self) -> int: + """Returns the total number of samples in the dataset.""" + return len(self.img_cache) + + def __getitem__(self, idx: int) -> IAM_Dataset_Element: + """ + Retrieves a sample from the dataset. + + Args: + idx (int): The index of the sample to retrieve. + + Returns: + A tuple containing: + - The pre-processed image as a NumPy array. + - A list of AABB objects for the ground truth words. + """ + image = self.img_cache[idx] + bounding_boxes = self.gt_cache[idx] + if self.transform: + # print('[INFO] transformation applied') + image, bounding_boxes = self.transform(image, bounding_boxes) + gt_encoded = encode(self.input_size, self.output_size, bounding_boxes) + return { + "image": image, + "bounding_boxes": bounding_boxes, + "filename": self.filename_cache[idx], + "gt_encoded": gt_encoded, + } + + def store_element_as_image( + self, + idx: int, + output_path: Path, + draw_bboxes: bool = False, + store_gt_encoded: bool = False, + ) -> List[Path]: + """ + Saves a dataset element as an image with bounding boxes drawn on it. + + Args: + idx (int): The index of the dataset element to save. + output_path (Path): The path where the image should be saved. + """ + # Get the element + element = self[idx] + img = element["image"].copy() # Copy to avoid modifying the cached image + bboxes = element["bounding_boxes"] + + # Convert grayscale to BGR for colored bounding boxes + img_color = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + + # Draw bounding boxes + if draw_bboxes: + for bbox in bboxes: + # Convert float coordinates to integers + x_min = int(bbox.x_min) + y_min = int(bbox.y_min) + x_max = int(bbox.x_max) + y_max = int(bbox.y_max) + + # Draw rectangle (green color, thickness=2) + cv2.rectangle(img_color, (x_min, y_min), (x_max, y_max), (0, 255, 0), 2) + + # Save the image + cv2.imwrite(str(output_path), img_color) + files_to_return = [output_path] + + # TODO: Add text there + + if store_gt_encoded: + gt_encoded = element["gt_encoded"] + for key, value in MapOrdering.__dict__.items(): + if "__" not in key and key != "NUM_MAPS": + data = gt_encoded[value].copy() + data_normalised = ( + (data - data.min()) / (data.max() - data.min()) * 255 + ).astype(np.uint8) # Required for storing as image + + name = Path( + output_path.stem + f"__{key.lower()}" + output_path.suffix + ) + files_to_return.append(name) + + # Convert grayscale to BGR for colored bounding boxes + img_color = cv2.cvtColor(data_normalised, cv2.COLOR_GRAY2BGR) + + # Save the image + cv2.imwrite(str(name), img_color) + + return files_to_return + + +def dummy_transform(img, aabbs): + return img, aabbs + + +class Dataloader_Element(TypedDict): + images: torch.tensor + bounding_boxes: List[List[BoundingBox]] + gt_encoded: torch.tensor + + +def custom_collate_fn(batch: List[IAM_Dataset_Element]) -> Dataloader_Element: + """ + Custom collate function to handle IAM_Dataset_Element batches. + """ + + batch_images = [] + batch_gt_encodeds = [] + batch_bounding_boxes = [] + + for sample in batch: + image = sample["image"] + gt_encoded = sample["gt_encoded"] + bounding_boxes = sample["bounding_boxes"] + batch_images.append(image[None, ...].astype(np.float32)) + batch_gt_encodeds.append(gt_encoded.astype(np.float32)) + batch_bounding_boxes.append(bounding_boxes) + + batch_images = np.stack(batch_images, axis=0) + batch_gt_encodeds = np.stack(batch_gt_encodeds, axis=0) + + batch_images = torch.from_numpy(batch_images) + batch_gt_encodeds = torch.from_numpy(batch_gt_encodeds) + + return { + "images": batch_images, + "gt_encoded": batch_gt_encodeds, + "bounding_boxes": batch_bounding_boxes, + } + + +def count_parameters(net): + total_params = sum(p.numel() for p in net.parameters()) + trainable_params = sum(p.numel() for p in net.parameters() if p.requires_grad) + return { + "total_params": total_params, + "trainable_params": trainable_params, + } + + +def compute_iou(ra: BoundingBox, rb: BoundingBox) -> float: + """intersection over union of two axis aligned rectangles ra and rb""" + if ( + ra.x_max < rb.x_min + or rb.x_max < ra.x_min + or ra.y_max < rb.y_min + or rb.y_max < ra.y_min + ): + return 0 + + l = max(ra.x_min, rb.x_min) # noqa: E741 + r = min(ra.x_max, rb.x_max) + t = max(ra.y_min, rb.y_min) + b = min(ra.y_max, rb.y_max) + + intersection = (r - l) * (b - t) + union = ra.area() + rb.area() - intersection + + iou = intersection / union + return iou + + +def compute_dist_mat(aabbs: List[BoundingBox]) -> np.ndarray: + """Jaccard distance matrix of all pairs of aabbs""" + num_aabbs = len(aabbs) + + dists = np.zeros((num_aabbs, num_aabbs)) + for i in range(num_aabbs): + for j in range(num_aabbs): + if j > i: + break + + dists[i, j] = dists[j, i] = 1 - compute_iou(aabbs[i], aabbs[j]) + + return dists + + +def cluster_aabbs(aabbs: List[BoundingBox]) -> List[BoundingBox]: + """cluster aabbs using DBSCAN and the Jaccard distance between bounding boxes""" + if len(aabbs) < 2: + return aabbs + + dists = compute_dist_mat(aabbs) + clustering = DBSCAN(eps=0.7, min_samples=3, metric="precomputed").fit(dists) + + clusters = defaultdict(list) + for i, c in enumerate(clustering.labels_): + if c == -1: + continue + clusters[c].append(aabbs[i]) + + res_aabbs = [] + for curr_cluster in clusters.values(): + xmin = np.median([aabb.x_min for aabb in curr_cluster]) + xmax = np.median([aabb.x_max for aabb in curr_cluster]) + ymin = np.median([aabb.y_min for aabb in curr_cluster]) + ymax = np.median([aabb.y_max for aabb in curr_cluster]) + res_aabbs.append(BoundingBox(x_min=xmin, x_max=xmax, y_min=ymin, y_max=ymax)) + + return res_aabbs + + +def compute_dist_mat_2( + aabbs1: List[BoundingBox], aabbs2: List[BoundingBox] +) -> np.ndarray: + """Jaccard distance matrix of all pairs of aabbs from lists aabbs1 and aabbs2""" + num_aabbs1 = len(aabbs1) + num_aabbs2 = len(aabbs2) + + dists = np.zeros((num_aabbs1, num_aabbs2)) + for i in range(num_aabbs1): + for j in range(num_aabbs2): + dists[i, j] = 1 - compute_iou(aabbs1[i], aabbs2[j]) + + return dists + + +def binary_classification_metrics( + gt_aabbs: List[BoundingBox], pred_aabbs: List[BoundingBox] +) -> dict: + iou_thres = 0.7 + + ious = 1 - compute_dist_mat_2(gt_aabbs, pred_aabbs) + match_counter = (ious > iou_thres).astype(int) + gt_counter = np.sum(match_counter, axis=1) + pred_counter = np.sum(match_counter, axis=0) + + tp = np.count_nonzero(pred_counter == 1) + fp = np.count_nonzero(pred_counter == 0) + fn = np.count_nonzero(gt_counter == 0) + + return { + "tp": tp, + "fp": fp, + "fn": fn, + } + + +def draw_bboxes_on_image( + img: np.ndarray, + aabbs: List[BoundingBox], + denormalise: bool = True, +) -> np.ndarray: + """ + Draws bounding boxes on an image. + + Args: + img (np.ndarray): The image on which to draw the bounding boxes. + aabbs (List[BoundingBox]): List of bounding boxes to draw. + + Returns: + np.ndarray: The image with drawn bounding boxes. + """ + img = img.copy() + if denormalise: + img = ((img + 0.5) * 255).astype(np.uint8) # Reverse normalization + is_grayscale = ( + len(img.shape) == 2 + ) # Otherwise the image is interpreted as BGR b/c we use cv2 + if is_grayscale: + img = cv2.cvtColor(img, cv2.COLOR_GRAY2BGR) + + for aabb in aabbs: + aabb = aabb.enlarge_to_int_grid().as_type( + int + ) # TODO: as_type doesn't work, grr + + cv2.rectangle( + img, + ( + int(aabb.x_min), + int(aabb.y_min), + ), + ( + int(aabb.x_max), + int(aabb.y_max), + ), + (0, 0, 255), # Red + 2, + ) + + return img + + +def compute_loss(y, gt_map): + # TODO: refactoring possibility: compute area, can be used 2 to 3 times + # TODO: add weights in loss and hyperparameter-tune them + + # 1. segmentation loss + target_labels = torch.argmax( + gt_map[:, MapOrdering.SEG_WORD : MapOrdering.SEG_BACKGROUND + 1], dim=1 + ) + loss_seg = F.cross_entropy( + y[:, MapOrdering.SEG_WORD : MapOrdering.SEG_BACKGROUND + 1], target_labels + ) + + # 2. geometry loss + # distances to all sides of aabb + t = torch.minimum(y[:, MapOrdering.GEO_TOP], gt_map[:, MapOrdering.GEO_TOP]) + b = torch.minimum(y[:, MapOrdering.GEO_BOTTOM], gt_map[:, MapOrdering.GEO_BOTTOM]) + l = torch.minimum(y[:, MapOrdering.GEO_LEFT], gt_map[:, MapOrdering.GEO_LEFT]) # noqa: E741 + r = torch.minimum(y[:, MapOrdering.GEO_RIGHT], gt_map[:, MapOrdering.GEO_RIGHT]) + + # area of predicted aabb + y_width = y[:, MapOrdering.GEO_LEFT, ...] + y[:, MapOrdering.GEO_RIGHT, ...] + y_height = y[:, MapOrdering.GEO_TOP, ...] + y[:, MapOrdering.GEO_BOTTOM, ...] + area1 = y_width * y_height + + # area of gt aabb + gt_width = ( + gt_map[:, MapOrdering.GEO_LEFT, ...] + gt_map[:, MapOrdering.GEO_RIGHT, ...] + ) + gt_height = ( + gt_map[:, MapOrdering.GEO_TOP, ...] + gt_map[:, MapOrdering.GEO_BOTTOM, ...] + ) + area2 = gt_width * gt_height + + # compute intersection over union + intersection = (r + l) * (b + t) + union = area1 + area2 - intersection + eps = 0.01 # avoid division by 0 + iou = intersection / (union + eps) + iou = iou[gt_map[:, MapOrdering.SEG_WORD] > 0] + loss_aabb = -torch.log(torch.mean(iou)) + + # total loss is simply the sum of both losses + loss = loss_seg + loss_aabb + return loss + + +# If you were using Bottleneck for other ResNet versions: +# from torchvision.models.resnet import ResNet, BasicBlock, Bottleneck + + +class ModifiedResNet18(ResNet): + def __init__(self, **kwargs): + # Initialize with BasicBlock and standard ResNet-18 layers + # num_classes is irrelevant here as we won't use the fc layer + super().__init__(BasicBlock, [2, 2, 2, 2], num_classes=1000, **kwargs) + + # 1. Modify the first convolutional layer for 1-channel (grayscale) input + # Original resnet.conv1 is Conv2d(3, 64, kernel_size=7, stride=2, padding=3, bias=False) + # We need Conv2d(1, 64, ...) + original_conv1 = self.conv1 + self.conv1 = nn.Conv2d( + 1, + original_conv1.out_channels, + kernel_size=original_conv1.kernel_size, + stride=original_conv1.stride, + padding=original_conv1.padding, + bias=False, + ) # bias is False in original ResNet conv1 + + # Optional: If you wanted to initialize weights similarly to torchvision: + # nn.init.kaiming_normal_(self.conv1.weight, mode='fan_out', nonlinearity='relu') + # However, if you load custom pretrained weights for the whole model later, + # this specific initialization might be overwritten. + + # We don't need the final fully connected layer for feature extraction + del self.fc + # self.avgpool is also not strictly needed for the U-Net style features, + # but it doesn't hurt to keep it if not used. You could 'del self.avgpool' too. + + def _forward_impl(self, x: torch.Tensor): + # This is largely copied from torchvision.models.resnet.ResNet._forward_impl + # but modified to return intermediate features. + + # See note [TorchScript super()] + x = self.conv1(x) + x = self.bn1(x) + out1 = self.relu(x) # Corresponds to bb1 in WordDetectorNet (before maxpool) + x = self.maxpool(out1) + + out2 = self.layer1(x) # Corresponds to bb2 + out3 = self.layer2(out2) # Corresponds to bb3 + out4 = self.layer3(out3) # Corresponds to bb4 + out5 = self.layer4(out4) # Corresponds to bb5 + + # WordDetectorNet expects (bb5, bb4, bb3, bb2, bb1) + return out5, out4, out3, out2, out1 + + def forward(self, x: torch.Tensor): + return self._forward_impl(x) + + +def compute_scale_down(input_size, output_size): + """compute scale down factor of neural network, given input and output size""" + return output_size[0] / input_size[0] + + +class UpscaleAndConcatLayer(torch.nn.Module): + """ + take small map with cx channels + upscale to size of large map (s*s) + concat large map with cy channels and upscaled small map + apply conv and output map with cz channels + """ + + def __init__(self, cx, cy, cz): + super(UpscaleAndConcatLayer, self).__init__() + self.conv = torch.nn.Conv2d(cx + cy, cz, 3, padding=1) + + def forward(self, x, y, s): + x = F.interpolate(x, s) + z = torch.cat((x, y), 1) + z = F.relu(self.conv(z)) + return z + + +class WordDetectorNet(torch.nn.Module): + input_size = (448, 448) + output_size = (224, 224) + # v-- TODO: It's a hack to keep both. I do so now b/c I don't know the order (also, doesn't matter b/c same values) + input_size_ImageDimensions: ImageDimensions = ImageDimensions(width=448, height=448) + output_size_ImageDimensions: ImageDimensions = ImageDimensions( + width=224, height=224 + ) + scale_down = compute_scale_down(input_size, output_size) + + def __init__(self): + super(WordDetectorNet, self).__init__() + + # Use the modified ResNet18 for feature extraction + self.backbone = ModifiedResNet18() + # All weights in the backbone will be randomly initialized. + + self.up1 = UpscaleAndConcatLayer(512, 256, 256) # input//16 + self.up2 = UpscaleAndConcatLayer(256, 128, 128) # input//8 + self.up3 = UpscaleAndConcatLayer(128, 64, 64) # input//4 + self.up4 = UpscaleAndConcatLayer(64, 64, 32) # input//2 + + self.conv1 = torch.nn.Conv2d(32, MapOrdering.NUM_MAPS, 3, 1, padding=1) + + @staticmethod + def scale_shape(s, f): + assert s[0] % f == 0 and s[1] % f == 0 + return s[0] // f, s[1] // f + + def output_activation(self, x, apply_softmax): + if apply_softmax: + seg = torch.softmax( + x[:, MapOrdering.SEG_WORD : MapOrdering.SEG_BACKGROUND + 1], dim=1 + ) + else: + seg = x[:, MapOrdering.SEG_WORD : MapOrdering.SEG_BACKGROUND + 1] + geo = ( + torch.sigmoid(x[:, MapOrdering.GEO_TOP :]) * self.input_size[0] + ) # TODO: Understand this + y = torch.cat([seg, geo], dim=1) + return y + + def forward(self, x, apply_softmax=False): + s = x.shape[2:] # Original image shape HxW + bb5, bb4, bb3, bb2, bb1 = self.backbone(x) + + y = self.up1(bb5, bb4, self.scale_shape(s, 16)) + # up2 takes y (H/16, 256ch) and bb3 (H/8, 128ch). Upscales y to H/8. Output: H/8, 128ch. + y = self.up2(y, bb3, self.scale_shape(s, 8)) + # up3 takes y (H/8, 128ch) and bb2 (H/4, 64ch). Upscales y to H/4. Output: H/4, 64ch. + y = self.up3(y, bb2, self.scale_shape(s, 4)) + # up4 takes y (H/4, 64ch) and bb1 (H/2, 64ch). Upscales y to H/2. Output: H/2, 32ch. + y = self.up4(y, bb1, self.scale_shape(s, 2)) + + y = self.conv1( + y + ) # Final convolution to get NUM_MAPS channels. Output: H/2, NUM_MAPS ch. + + return self.output_activation(y, apply_softmax) + + +# ========== +# Transforms +# ========== + + +def normalize_image_transform(image, bounding_boxes): + image_new = (image / 255.0) - 0.5 + return image_new, bounding_boxes + + +# ========= +# Inference +# ========= + + +def run_image_through_network( + image_grayscale: np.ndarray, + model_path: Path = Path("best_model.pth"), + device: str = "cuda", +) -> List[BoundingBox]: + # Load model + # ========== + + model = WordDetectorNet() # instantiate your model + model.load_state_dict(torch.load(model_path, map_location=device)) + model.to(device) + model.eval() + + # ============== + # Pre processing + # ============== + + image_gray_rescaled = cv2.resize(image_grayscale, WordDetectorNet.input_size) + + image_grayscale_transformed, _ = normalize_image_transform( + image_gray_rescaled, None + ) # Only works w/ current transformation setup + + image_grayscale_transformed = image_grayscale_transformed.astype(np.float32) + + image_grayscale_transformed = torch.from_numpy( + image_grayscale_transformed[None, None, :, :] + ).to(device) + + # ========= + # Inference + # ========= + + with torch.no_grad(): + output_image = model(image_grayscale_transformed, apply_softmax=True) + + assert ( + output_image[ + :, MapOrdering.SEG_WORD : MapOrdering.SEG_BACKGROUND + 1, :, : + ].min() + >= 0.0 + ) + assert ( + output_image[ + :, MapOrdering.SEG_WORD : MapOrdering.SEG_BACKGROUND + 1, :, : + ].max() + <= 1.0 + ) + + output_image = output_image.to("cpu").numpy() + + output_image = output_image[0, :, :, :] + + # =============== + # Post processing + # =============== + + decoded_aabbs = decode( + output_image, + scale=WordDetectorNet.input_size[0] / WordDetectorNet.output_size[0], + comp_fg=fg_by_cc(thres=0.5, max_num=1000), + ) + model_input_image = image_grayscale_transformed[0, 0, :, :].to("cpu").numpy() + h, w = model_input_image.shape + aabbs = [ + aabb.clip(BoundingBox(0, 0, w - 1, h - 1)) for aabb in decoded_aabbs + ] # bounding box must be inside input img + clustered_aabbs = cluster_aabbs(aabbs) + + return { + "aabbs": clustered_aabbs, + "model_input_image": model_input_image, + } + + +class CustomEncoder(json.JSONEncoder): + """This is to make non-standard items serialisable for `json.dump(s)`.""" + + def default(self, obj): + if isinstance(obj, Path): # Store `Path` objects + return str(obj) + return super().default(obj) + + +def get_git_commit_hash(repo_path: Path = Path("."), short: bool = False) -> str: + """ + Get the current Git commit hash using GitPython. + + :param repo_path: Path to the Git repository (default: current directory). + :param short: Whether to return the short hash. + :return: Commit hash string, or None if unavailable. + """ + try: + repo = Repo(repo_path, search_parent_directories=True) + commit_hash: str = repo.head.commit.hexsha + return commit_hash[:7] if short else commit_hash + except Exception as e: + print(f"Error while getting git commit hash from {repo_path}: {e}") + return "-1" + + +def url_exists(url: str) -> bool: + """Check if a URL exists using a HEAD request.""" + try: + req = urllib.request.Request(url, method="HEAD") + with urllib.request.urlopen(req, timeout=5) as response: + return response.status == 200 + except Exception as e: + print(e) + return False + + +def get_example_list() -> List[List]: + """Only return examples that still exist online.""" + links_to_images = [ + "https://raw.githubusercontent.com/githubharald/WordDetectorNN/master/data/test/cvl.jpg", + "https://raw.githubusercontent.com/githubharald/WordDetectorNN/master/data/test/random.jpg", + "https://raw.githubusercontent.com/githubharald/WordDetectorNN/master/data/test/bentham.jpg", + ] + result = [] + for link in links_to_images: + image_exists = url_exists(link) + if image_exists: + result.append([link, 0, False]) + return result + + +def save_event( + data: dict, + SB_URL: str, + SB_KEY: str, + SB_SCHEMA_NAME: str, + SB_TABLE_NAME: str, + SB_BUCKET_NAME: str, +): + supabase: Client = create_client(SB_URL, SB_KEY) + + uid = str(data["uuid"]) + contains_image = data.get("image") is not None + + if contains_image and data["donate_data"]: + arr = data["image"] + if not isinstance(arr, np.ndarray): + raise ValueError("Image must be a numpy array!") + + # Save NumPy array into .npy format in memory + buf = io.BytesIO() + np.save(buf, arr) + buf.seek(0) + + # Upload to Supabase Storage (pass actual bytes, not BytesIO) + filename = f"{uid}.npy" + supabase.storage.from_(SB_BUCKET_NAME).upload( + filename, + buf.getvalue(), + {"content-type": "application/octet-stream"}, + ) + + # Insert metadata row + row = { + "timestamp": data["timestamp"].isoformat(), + "demo": data["demo"], + "uuid": uid, + "donate_data": data["donate_data"], + "contains_image": contains_image, + } + + supabase.schema(SB_SCHEMA_NAME).table(SB_TABLE_NAME).insert(row).execute() diff --git a/xournalpp_htr/training/WordDetectorNN/pyproject.toml b/xournalpp_htr/training/WordDetectorNN/pyproject.toml new file mode 100644 index 0000000000000000000000000000000000000000..ff6b26c2746cb840023cdcc657a1a866f48f4183 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "worddetectornn" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "dotenv>=0.9.9", + "gitpython>=3.1.45", + "gradio>=5.47.2", + "jupyter>=1.1.1", + "opencv-python>=4.12.0.88", + "scikit-learn>=1.7.2", + "supabase>=2.20.0", + "tensorboard>=2.20.0", + "torch>=2.7.1", + "torchaudio>=2.7.1", + "torchvision>=0.22.1", + "tqdm>=4.67.1", +] diff --git a/xournalpp_htr/training/WordDetectorNN/requirements.txt b/xournalpp_htr/training/WordDetectorNN/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..2918be8c373e80a0447912ef07b449281db5d017 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/requirements.txt @@ -0,0 +1,10 @@ +# For HF Gradio Space +torch +torchvision +numpy +scikit-learn +tqdm +opencv-python +gitpython +supabase +dotenv \ No newline at end of file diff --git a/xournalpp_htr/training/WordDetectorNN/run_training.py b/xournalpp_htr/training/WordDetectorNN/run_training.py new file mode 100644 index 0000000000000000000000000000000000000000..8d41882702eb68fc826a528899659bb2b6a36e93 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/run_training.py @@ -0,0 +1,401 @@ +import argparse +import json +from pathlib import Path +import random +import time + +import torch +from torch.utils.data import Subset +import numpy as np +from torch.utils.data import DataLoader +from datetime import datetime +from torch.utils.tensorboard import SummaryWriter +import matplotlib.pyplot as plt + +from my_code import IAM_Dataset +from my_code import ImageDimensions +from my_code import custom_collate_fn +from my_code import count_parameters +from my_code import fg_by_cc +from my_code import cluster_aabbs +from my_code import binary_classification_metrics +from my_code import draw_bboxes_on_image +from my_code import MapOrdering +from my_code import encode, decode, BoundingBox, ImageDimensions +from my_code import ModifiedResNet18 +from my_code import WordDetectorNet +from my_code import compute_loss +from my_code import normalize_image_transform +from my_code import CustomEncoder +from my_code import get_git_commit_hash + + +global_step = 0 # TODO: Make global step non-global as it's very bad practise. + +def parse_args() -> dict: + parser = argparse.ArgumentParser( + description="Train a WordDetectorNet model.", + formatter_class=argparse.ArgumentDefaultsHelpFormatter, + ) + + training_group = parser.add_argument_group("Training Settings") + training_group.add_argument("--learning_rate", type=float, default=0.001, + help="Learning rate for optimizer") + training_group.add_argument("--val_epoch", type=int, default=1, + help="Validation frequency in epochs") + training_group.add_argument("--epoch_max", type=int, default=3, + help="Maximum number of epochs") + training_group.add_argument("--patience_max", type=int, default=50, + help="Early stopping patience") + training_group.add_argument("--batch_size", type=int, default=32, + help="Batch size") + training_group.add_argument("--num_workers", type=int, default=1, + help="Number of data loader workers") + + data_group = parser.add_argument_group("Data Settings") + data_group.add_argument("--data_path", type=Path, + default=Path.home() / "Development/WordDetectorNN/data/train", + help="Path to training data") + data_group.add_argument("--percent_train_data", type=int, default=80, + help="Percentage of data used for training") + data_group.add_argument("--no-shuffle-data-loader", dest="shuffle_data_loader", + action="store_false", + help="Disable shuffling of data loader (default: enabled)") + parser.set_defaults(shuffle_data_loader=True) + data_group.add_argument("--cache_path", type=Path, + default=Path.home() / "dataset_cache.pickle", + help="Path to dataset cache file") + + seed_group = parser.add_argument_group("Reproducibility Settings") + seed_group.add_argument("--seed_split", type=int, default=42, + help="Random seed for dataset splitting") + seed_group.add_argument("--seed_model", type=int, default=1337, + help="Random seed for model initialization") + + output_group = parser.add_argument_group("Output Settings") + output_group.add_argument("--output_path", type=Path, default=Path("test_output_path"), + help="Output directory (will be created if it doesn't exist)") + + args = parser.parse_args() + return vars(args) + +def seed_everything(numpy_seed=42, torch_seed=1234, random_seed=7): + random.seed(random_seed) + np.random.seed(numpy_seed) + torch.manual_seed(torch_seed) + torch.cuda.manual_seed(torch_seed) + torch.cuda.manual_seed_all(torch_seed) + torch.backends.cudnn.deterministic = True + torch.backends.cudnn.benchmark = False + print(f"Seeds set -> random: {random_seed}, numpy: {numpy_seed}, torch: {torch_seed}") + +def get_dataloaders( + data_path: Path, + percent_train_data: int, + batch_size: int, + shuffle_data_loader: bool, + num_workers: int, + output_path: Path, + cache_path: Path, +) -> dict: + + # -- datasets -- + + # Create datasets with different transforms + train_transform = normalize_image_transform + val_transform = normalize_image_transform + # TODO: ^ Implement the augmentations, w/ each changing at every batch + + train_dataset = IAM_Dataset( + root_dir=data_path, + input_size=WordDetectorNet.input_size_ImageDimensions, + output_size=WordDetectorNet.output_size_ImageDimensions, + force_rebuild_cache=False, + transform=train_transform, + cache_path=cache_path, + ) + val_dataset = IAM_Dataset( + root_dir=data_path, + input_size=WordDetectorNet.input_size_ImageDimensions, + output_size=WordDetectorNet.output_size_ImageDimensions, + force_rebuild_cache=False, + transform=val_transform, + cache_path=cache_path, + ) + + assert len(train_dataset) == len(val_dataset) + + indices = list(range(len(train_dataset))) + np.random.shuffle(indices) + split = int(percent_train_data / 100 * len(indices)) + + train_indices = indices[:split] + val_indices = indices[split:] + + with open(output_path / 'dataset_split_indices.json', 'w') as f: + split_indices = { + 'train': train_indices, + 'val': val_indices, + } + json.dump(split_indices, f, indent=4) + + train_subset = Subset(train_dataset, train_indices) + val_subset = Subset(val_dataset, val_indices) + + train_filenames = [sample['filename'] for sample in train_subset] + val_filenames = [sample['filename'] for sample in val_subset] + # Check that no train samples are in val + assert len(set(train_filenames + val_filenames)) == len(train_filenames) + len(val_filenames) + + # -- dataloaders -- + + dataloader_train = DataLoader( + train_subset, + batch_size=batch_size, + shuffle=shuffle_data_loader, + num_workers=num_workers, + collate_fn=custom_collate_fn, + pin_memory=True # For faster GPU transfer + ) + + dataloader_val = DataLoader( + val_subset, + batch_size=batch_size, + shuffle=False, # no need to shuffle validation data and otherwise images break + num_workers=num_workers, + collate_fn=custom_collate_fn, + pin_memory=True # For faster GPU transfer + ) + + return { + 'train': dataloader_train, + 'val': dataloader_val, + } + +# TODO: Even w/o benchmarking, I can say that this is the bottleneck here. Also, it's +# not the GPU part that is the bottleneck, but the CPU part. How about making +# the loop annotated with (A) run in parallel? +def validate(net, dataloader_val, writer, device, input_size, output_size, regularisation=1e-8): + global global_step + net.eval() + avg_loss = 0.0 + tp = 0 + fp = 0 + fn = 0 + image_counter = 0 + for i, batch in enumerate(dataloader_val): + # For loss + with torch.no_grad(): + images = batch['images'] + gt_encoded = batch['gt_encoded'] + images = images.to(device) + gt_encoded = gt_encoded.to(device) + y = net(images) + loss = compute_loss(y, gt_encoded) + avg_loss += loss.item() + # For metrics; TODO: Can be combined by performing softmax on y from above but too lazy right now + with torch.no_grad(): + images = batch['images'] + gt_encoded = batch['gt_encoded'] + images = images.to(device) + gt_encoded = gt_encoded.to(device) + y = net(images, apply_softmax=True) + assert y[:, MapOrdering.SEG_WORD:MapOrdering.SEG_BACKGROUND+1, :, :].min() >= 0.0 + assert y[:, MapOrdering.SEG_WORD:MapOrdering.SEG_BACKGROUND+1, :, :].max() <= 1.0 + batch_size_here = y.shape[0] + y = y.to('cpu').numpy() + for i_element_in_batch in range(batch_size_here): # <-- (A) + y_element = y[i_element_in_batch, :, :, :] + decoded_aabbs = decode(y_element, scale=input_size.width / output_size.width, comp_fg=fg_by_cc(thres=0.5, max_num=1000)) + img_np = batch['images'][i_element_in_batch, 0, :, :].to('cpu').numpy() + h, w = img_np.shape + aabbs = [aabb.clip(BoundingBox(0, 0, w - 1, h - 1)) for aabb in decoded_aabbs] # bounding box must be inside img + clustered_aabbs = cluster_aabbs(aabbs) + result = binary_classification_metrics(batch['bounding_boxes'][i_element_in_batch], clustered_aabbs) + tp += result['tp'] + fp += result['fp'] + fn += result['fn'] + vis = draw_bboxes_on_image(img_np, clustered_aabbs) + writer.add_image(f'img{image_counter}', vis.transpose((2, 0, 1)), global_step) + image_counter += 1 + avg_loss = avg_loss / len(dataloader_val) + precision = tp / (tp + fp + regularisation) + recall = tp / (tp + fn + regularisation) + f1 = 2*precision*recall / (precision + recall + regularisation) + writer.add_scalar('loss/val', avg_loss, global_step) + writer.add_scalar('f1/val', f1, global_step) + return f1 + +def train(net, optimizer, loader, writer, device): + global global_step + + net.train() + for i, loader_item in enumerate(loader): + + images = loader_item['images'] + gt_encoded = loader_item['gt_encoded'] + + images = images.to(device) + gt_encoded = gt_encoded.to(device) + + # forward pass + optimizer.zero_grad() + y = net(images) + loss = compute_loss(y, gt_encoded) + + # backward pass, optimize loss + loss.backward() + optimizer.step() + + # output + print(f'{i + 1}/{len(loader)}: {loss.item()}') + writer.add_scalar('loss/train', loss, global_step) + global_step += 1 + +def train_network( + output_path: Path, + device: str, + learning_rate: float, + batch_size: int, + dataloader_train, + dataloader_val, + epoch_max: int, + patience_max: int, + val_epoch: int, + seed_split: int, + seed_model: int, +): + writer = SummaryWriter(output_path / 'summary_writer') + + net = WordDetectorNet() + net.to(device) + + # optimizer + optimizer = torch.optim.Adam(net.parameters(), lr=learning_rate) + + # main training loop + epoch = 0 + best_val_f1 = 0.0 + patience_counter = 0 + while True: + epoch += 1 + print(f'Epoch: {epoch}') + train(net, optimizer, dataloader_train, writer, device) + if epoch % val_epoch == 0: + f1 = validate(net, dataloader_val, writer, device, WordDetectorNet.input_size_ImageDimensions, WordDetectorNet.output_size_ImageDimensions) + + if f1 > best_val_f1: + print(f"New best F1 score: {best_val_f1:.4f} -> {f1:.4f}, saving model.") + best_val_f1 = f1 + torch.save(net.state_dict(), output_path / 'best_model.pth') + patience_counter = 0 + with open(output_path / 'best_model.json', 'w') as f: + json.dump( + { + 'epoch': epoch, + 'global_step': global_step, + 'f1': best_val_f1, + }, + f, + indent=4, + ) + else: + patience_counter += 1 + + if patience_counter >= patience_max: + print("Early stopping triggered.") + break + + if epoch >= epoch_max: + print(f"Reached max epoch {epoch_max}, stopping training.") + break + + writer.add_hparams( + { + 'learning_rate': learning_rate, + 'batch_size': batch_size, + 'seed_split': seed_split, + 'seed_model': seed_model, + 'patience_max': patience_max, + }, + { + 'best_val_f1': best_val_f1, + } + ) + + writer.close() + + # TODO: Later, replace all print statements w/ proper logging statements. + +def main(args: dict): + + args['output_path'].mkdir(exist_ok=True, parents=True) + + with open(args['output_path'] / 'args.json', 'w') as f: + json.dump( + args, + f, + indent=4, + cls=CustomEncoder, + ) + + device = 'cuda' if torch.cuda.is_available() else 'cpu' + + seed_everything( + numpy_seed=args['seed_split'], + torch_seed=args['seed_model'], + random_seed=args['seed_model'], + ) + + t0 = time.time() + dataloaders = get_dataloaders( + data_path=args['data_path'], + percent_train_data=args['percent_train_data'], + batch_size=args['batch_size'], + shuffle_data_loader=args['shuffle_data_loader'], + num_workers=args['num_workers'], + output_path=args['output_path'], + cache_path=args['cache_path'], + ) + dataloaders_time = time.time() - t0 + + t0 = time.time() + train_network( + output_path=args['output_path'], + device=device, + learning_rate=args['learning_rate'], + batch_size=args['batch_size'], + dataloader_train=dataloaders['train'], + dataloader_val=dataloaders['val'], + epoch_max=args['epoch_max'], + patience_max=args['patience_max'], + val_epoch=args['val_epoch'], + seed_split=args['seed_split'], + seed_model=args['seed_model'], + ) + training_time = time.time() - t0 + + with open(args['output_path'] / 'git_commit_hash.json', 'w') as f: + json.dump( + { + 'git_commit_hash': get_git_commit_hash(), + }, + f, + indent=4, + cls=CustomEncoder, + ) + + with open(args['output_path'] / 'times.json', 'w') as f: + json.dump( + { + 'dataloaders': dataloaders_time, + 'training': training_time, + }, + f, + indent=4, + cls=CustomEncoder, + ) + +if __name__ == '__main__': + args = parse_args() + main(args) \ No newline at end of file diff --git a/xournalpp_htr/training/WordDetectorNN/run_training.sh b/xournalpp_htr/training/WordDetectorNN/run_training.sh new file mode 100644 index 0000000000000000000000000000000000000000..43827ba04d4af4015dc46a3616098282738bb0cf --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/run_training.sh @@ -0,0 +1,43 @@ +# TODO: Store log into file as script itself does not capture the log itself + +set -e # To catch all errors + +# ======== +# Settings +# ======== + +BASE_PATH=${HOME}/experiments + +# ============ +# Experiment 1 +# ============ + +# Question: General hyperparameter tuning + +EPOCH_MAX=200 + +for LEARNING_RATE in 0.0005 0.001 0.002 +do + for BATCH_SIZE in 16 32 64 128 + do + + echo "LR=${LEARNING_RATE}, BS=${BATCH_SIZE}" + + python run_training.py \ + --learning_rate ${LEARNING_RATE} \ + --batch_size ${BATCH_SIZE} \ + --output_path ${BASE_PATH}/experiment1/lr${LEARNING_RATE}_bs${BATCH_SIZE} \ + --epoch_max ${EPOCH_MAX} \ + + done +done + +# ================== +# Future experiments +# ================== + +# Other questions to answer by conducting additional experiments: +# - Do different model seeds change results? +# - Does batch size matter? -> already covered in general hyperparameter tuning +# - Longer training help w/ more patience? +# - Cheap as k-fold on data to get good estimate of true performance \ No newline at end of file diff --git a/xournalpp_htr/training/WordDetectorNN/tests.ipynb b/xournalpp_htr/training/WordDetectorNN/tests.ipynb new file mode 100644 index 0000000000000000000000000000000000000000..f4e05bc759f1ec7619f9afead2e86892501cb37b --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/tests.ipynb @@ -0,0 +1,719 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "539c5c9b", + "metadata": {}, + "source": [ + "# Tests\n", + "\n", + "Quick tests that can become future unit tests." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7feb9aa4", + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ab3f1774", + "metadata": {}, + "outputs": [], + "source": [ + "from pathlib import Path\n", + "\n", + "import torch\n", + "from torch.utils.data import Subset\n", + "import numpy as np\n", + "from torch.utils.data import DataLoader\n", + "from datetime import datetime\n", + "from torch.utils.tensorboard import SummaryWriter\n", + "import matplotlib.pyplot as plt\n", + "\n", + "from my_code import IAM_Dataset\n", + "from my_code import ImageDimensions\n", + "from my_code import custom_collate_fn\n", + "from my_code import count_parameters\n", + "from my_code import fg_by_cc\n", + "from my_code import cluster_aabbs\n", + "from my_code import binary_classification_metrics\n", + "from my_code import draw_bboxes_on_image\n", + "from my_code import MapOrdering\n", + "from my_code import encode, decode, BoundingBox, ImageDimensions\n", + "from my_code import ModifiedResNet18\n", + "from my_code import WordDetectorNet\n", + "from my_code import compute_loss" + ] + }, + { + "cell_type": "markdown", + "id": "5559f3ce", + "metadata": {}, + "source": [ + "## Architecture\n", + "\n", + "Here, I note down how I build the project to remind myself and others in the future. Here we go:\n", + "\n", + "```mermaid\n", + "graph TD\n", + " A[IAM Folder] --> B[Train Dataset = Dtr]\n", + " A --> C[Val Dataset = Dval]\n", + " B --> D[Dtr w transform, img&aabb = Dtr']\n", + " C --> E[Dval w transform, img&aabb = Dval']\n", + " D --> F[Train DataLoader] \n", + " E --> G[Val DataLoader] \n", + " E --> H[no transform except normalisation]\n", + " D --> I[geometric & photo]\n", + "```\n", + "\n", + "- transform in Dataset\n", + "- encode in collate of DataLoader" + ] + }, + { + "cell_type": "markdown", + "id": "ad157a94", + "metadata": {}, + "source": [ + "## Encoding & Decoding" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3bc74a0e", + "metadata": {}, + "outputs": [], + "source": [ + "aabbs = [\n", + " # BoundingBox( 0, 0, 10, 10, label='word1'),\n", + " # BoundingBox( 5, 5, 15, 15, label='word2'),\n", + " BoundingBox(20, 20, 30, 30, label='word3'),\n", + "]" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "338c88b8", + "metadata": {}, + "outputs": [], + "source": [ + "input_size = ImageDimensions(100, 100)\n", + "output_size = ImageDimensions(50, 50)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "ec055923", + "metadata": {}, + "outputs": [], + "source": [ + "aabbs_encoded = encode(input_size, output_size, aabbs)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4a214d9d", + "metadata": {}, + "outputs": [], + "source": [ + "plt.figure()\n", + "plt.imshow(aabbs_encoded[MapOrdering.SEG_WORD, :, :], cmap='gray')\n", + "plt.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f51cc45f", + "metadata": {}, + "outputs": [], + "source": [ + "assert input_size.width / output_size.width == input_size.height / output_size.height" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a36d2665", + "metadata": {}, + "outputs": [], + "source": [ + "decoded_aabbs = decode(aabbs_encoded, scale=input_size.width / output_size.width)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a017a236", + "metadata": {}, + "outputs": [], + "source": [ + "decoded_aabbs" + ] + }, + { + "cell_type": "markdown", + "id": "c3b2961b", + "metadata": {}, + "source": [ + "## Dataset" + ] + }, + { + "cell_type": "markdown", + "id": "f0dc41c4", + "metadata": {}, + "source": [ + "First, create the dataset:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "2467caab", + "metadata": {}, + "outputs": [], + "source": [ + "# Experiment w/ dataset class\n", + "data_path = Path.home() / 'Development/WordDetectorNN/data/train'\n", + "dataset = IAM_Dataset(\n", + " root_dir=data_path,\n", + " # input_size=ImageDimensions(width=640, height=448),\n", + " input_size=ImageDimensions(width=400, height=600),\n", + " output_size=ImageDimensions(width=200, height=300),\n", + " force_rebuild_cache=True,\n", + " transform=None,\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "96a61a88", + "metadata": {}, + "source": [ + "Next, access an element:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "94b2abb5", + "metadata": {}, + "outputs": [], + "source": [ + "idx = 578\n", + "idx = 0\n", + "idx = 325\n", + "sample = dataset[idx]" + ] + }, + { + "cell_type": "markdown", + "id": "2ff8f404", + "metadata": {}, + "source": [ + "Then, plot a sample:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55bc227e", + "metadata": {}, + "outputs": [], + "source": [ + "dataset.store_element_as_image(idx, Path('test.png'), draw_bboxes=True, store_gt_encoded=True)" + ] + }, + { + "cell_type": "markdown", + "id": "6a1bab0c", + "metadata": {}, + "source": [ + "Next, let's split the dataset into training and val datasets:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5bb211a1", + "metadata": {}, + "outputs": [], + "source": [ + "# This way to create the train and val datasets seems convoluted but is necessary to ensure\n", + "# that train and val get only their transforms. I know that it could be implemented more efficiently\n", + "# but that's not necessary give the small dataset.\n", + "#\n", + "# An alternative way to implement it is to build a TransformSubset which not only Subset's but also\n", + "# applies a separate transform.\n", + "#\n", + "# Note that it is not a good idea to hardcode these transforms b/c one might want to use the plain dataset,\n", + "# even if only for inspection\n", + "\n", + "# Create datasets with different transforms\n", + "train_transform = None\n", + "val_transform = None\n", + "# TODO: ^ Implement the augmentations, w/ each changing at every batch\n", + "\n", + "train_dataset = IAM_Dataset(\n", + " root_dir=data_path,\n", + " # input_size=ImageDimensions(width=640, height=448),\n", + " input_size=ImageDimensions(width=400, height=600),\n", + " output_size=ImageDimensions(width=200, height=300),\n", + " force_rebuild_cache=False,\n", + " transform=train_transform,\n", + ")\n", + "val_dataset = IAM_Dataset(\n", + " root_dir=data_path,\n", + " # input_size=ImageDimensions(width=640, height=448),\n", + " input_size=ImageDimensions(width=400, height=600),\n", + " output_size=ImageDimensions(width=200, height=300),\n", + " force_rebuild_cache=False,\n", + " transform=val_transform,\n", + ")\n", + "\n", + "percent_train_data = 80\n", + "\n", + "assert len(train_dataset) == len(val_dataset)\n", + "\n", + "indices = list(range(len(train_dataset)))\n", + "np.random.seed(42)\n", + "np.random.shuffle(indices)\n", + "split = int(percent_train_data / 100 * len(indices))\n", + "\n", + "train_indices = indices[:split]\n", + "val_indices = indices[split:]\n", + "\n", + "train_subset = Subset(train_dataset, train_indices)\n", + "val_subset = Subset(val_dataset, val_indices)\n", + "\n", + "train_filenames = [sample['filename'] for sample in train_subset]\n", + "val_filenames = [sample['filename'] for sample in val_subset]\n", + "# Check that no train samples are in val\n", + "assert len(set(train_filenames + val_filenames)) == len(train_filenames) + len(val_filenames)\n", + "\n", + "assert len(dataset) == len(train_subset) + len(val_subset)" + ] + }, + { + "cell_type": "markdown", + "id": "7e57d696", + "metadata": {}, + "source": [ + "## Dataloader" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5f91577b", + "metadata": {}, + "outputs": [], + "source": [ + "shuffle_data_loader = True\n", + "batch_size = 32\n", + "num_workers = 1" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27404d0a", + "metadata": {}, + "outputs": [], + "source": [ + "dataloader_train = DataLoader(\n", + " train_subset,\n", + " batch_size=batch_size,\n", + " shuffle=shuffle_data_loader,\n", + " num_workers=num_workers,\n", + " collate_fn=custom_collate_fn, # or custom_collate_fn_with_padding\n", + " pin_memory=True # For faster GPU transfer\n", + ")\n", + "\n", + "dataloader_val = DataLoader(\n", + " val_subset,\n", + " batch_size=batch_size,\n", + " shuffle=shuffle_data_loader,\n", + " num_workers=num_workers,\n", + " collate_fn=custom_collate_fn, # or custom_collate_fn_with_padding\n", + " pin_memory=True # For faster GPU transfer\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "7176d44a", + "metadata": {}, + "source": [ + "Check lenghts:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "785f0c68", + "metadata": {}, + "outputs": [], + "source": [ + "len(dataloader_train), len(train_subset), len(train_subset) / batch_size" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "360025b9", + "metadata": {}, + "outputs": [], + "source": [ + "len(dataloader_val), len(val_subset), len(val_subset) / batch_size" + ] + }, + { + "cell_type": "markdown", + "id": "679a53d3", + "metadata": {}, + "source": [ + "Load a single batch for testing & inspect it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5892d064", + "metadata": {}, + "outputs": [], + "source": [ + "batch_train = next(iter(dataloader_train))\n", + "batch_val = next(iter(dataloader_val))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "f5fff7f4", + "metadata": {}, + "outputs": [], + "source": [ + "batch_train.keys(), batch_val.keys()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "cbe460c7", + "metadata": {}, + "outputs": [], + "source": [ + "batch_train['images'].shape, batch_val['images'].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "308cb0e5", + "metadata": {}, + "outputs": [], + "source": [ + "batch_train['gt_encoded'].shape, batch_val['gt_encoded'].shape" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "af839ec0", + "metadata": {}, + "outputs": [], + "source": [ + "len(batch_train['bounding_boxes']), len(batch_val['bounding_boxes'])" + ] + }, + { + "cell_type": "markdown", + "id": "c5d1becb", + "metadata": {}, + "source": [ + "Iterate through whole dataloader once:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42c3c689", + "metadata": {}, + "outputs": [], + "source": [ + "for batch in dataloader_train:\n", + " pass" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d0d444e5", + "metadata": {}, + "outputs": [], + "source": [ + "for batch in dataloader_val:\n", + " pass" + ] + }, + { + "cell_type": "markdown", + "id": "63eb0120", + "metadata": {}, + "source": [ + "## Neural network" + ] + }, + { + "cell_type": "markdown", + "id": "b13b5009", + "metadata": {}, + "source": [ + "Try it out:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "a68b7703", + "metadata": {}, + "outputs": [], + "source": [ + "backbone = ModifiedResNet18()\n", + "\n", + "H, W = 400, 500\n", + "H, W = 448, 448\n", + "H, W = 600, 600\n", + "test_input = torch.randn((1, 1, H, W))\n", + "\n", + "output = backbone(test_input)\n", + "out5, out4, out3, out2, out1 = output\n", + "\n", + "print(\"Print output sizes:\")\n", + "for o in output:\n", + " print(\"\\t\", o.shape)\n", + "\n", + "nr_params = count_parameters(backbone)\n", + "print(f\"Total params: {nr_params['total_params']}\")\n", + "print(f\"Trainable params: {nr_params['trainable_params']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "5753ef8e", + "metadata": {}, + "source": [ + "Now off to the `WordDetectorNN` (for now just copied from external repo):" + ] + }, + { + "cell_type": "markdown", + "id": "8a3f4992", + "metadata": {}, + "source": [ + "Now test it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9659b02f", + "metadata": {}, + "outputs": [], + "source": [ + "net = WordDetectorNet()\n", + "\n", + "H, W = net.input_size\n", + "test_input = torch.randn((1, 1, H, W))\n", + "\n", + "output = net(test_input)\n", + "\n", + "print(\"Print output sizes:\", output.shape)\n", + "\n", + "nr_params = count_parameters(net)\n", + "print(f\"Total params: {nr_params['total_params']}\")\n", + "print(f\"Trainable params: {nr_params['trainable_params']}\")" + ] + }, + { + "cell_type": "markdown", + "id": "8e96fd07", + "metadata": {}, + "source": [ + "Test neural network with dataloader item:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "136f14c3", + "metadata": {}, + "outputs": [], + "source": [ + "batch_item = next(iter(dataloader_train))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "c8263222", + "metadata": {}, + "outputs": [], + "source": [ + "transform = None\n", + "dataset = IAM_Dataset(\n", + " root_dir=data_path,\n", + " input_size=ImageDimensions(width=448, height=448),\n", + " output_size=ImageDimensions(width=224, height=224),\n", + " force_rebuild_cache=True,\n", + " transform=transform,\n", + ")\n", + "\n", + "shuffle_data_loader = True\n", + "batch_size = 32\n", + "num_workers = 1\n", + "dataloader = DataLoader(\n", + " dataset,\n", + " batch_size=batch_size,\n", + " shuffle=shuffle_data_loader,\n", + " num_workers=num_workers,\n", + " collate_fn=custom_collate_fn, # or custom_collate_fn_with_padding\n", + " pin_memory=True # For faster GPU transfer\n", + ")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9360dd7e", + "metadata": {}, + "outputs": [], + "source": [ + "batch_item = next(iter(dataloader))" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3f4ade82", + "metadata": {}, + "outputs": [], + "source": [ + "net = WordDetectorNet()\n", + "\n", + "output = net(batch_item['images'])\n", + "\n", + "print(\"Print output sizes:\", output.shape)\n", + "print(\"`gt_encoded` shape:\", batch_item['gt_encoded'].shape)\n" + ] + }, + { + "cell_type": "markdown", + "id": "e7a2a7bb", + "metadata": {}, + "source": [ + "It turns out that we need that `1` dimension in the input because the backbone convolutional uses this.\n", + "\n", + "Next, confirm that the above forward pass also works on GPU:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "78f0f3e1", + "metadata": {}, + "outputs": [], + "source": [ + "net = WordDetectorNet()\n", + "net.to('cuda')\n", + "\n", + "images = batch_item['images']\n", + "images = images.to('cuda')\n", + "\n", + "output = net(images)\n", + "\n", + "print(\"Print output sizes:\", output.shape)\n", + "print(\"`gt_encoded` shape:\", batch_item['gt_encoded'].shape)\n" + ] + }, + { + "cell_type": "markdown", + "id": "649c6f7a", + "metadata": {}, + "source": [ + "Yes, this seems to work, great! Interestingly, it is much faster than on CPU: 14.0s vs 0.1s." + ] + }, + { + "cell_type": "markdown", + "id": "ae9c1805", + "metadata": {}, + "source": [ + "## Loss" + ] + }, + { + "cell_type": "markdown", + "id": "556a0631", + "metadata": {}, + "source": [ + "For now, just copied to first make it work and improve the implementation (maybe) later:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "b6699622", + "metadata": {}, + "outputs": [], + "source": [ + "y = output.to('cuda')\n", + "gt_map = batch_item['gt_encoded'].to('cuda')\n", + "l = compute_loss(y, gt_map)" + ] + }, + { + "cell_type": "markdown", + "id": "b42806e9", + "metadata": {}, + "source": [ + "OK, this seems to work." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "pytorch (3.12.10)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.12.10" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/xournalpp_htr/training/WordDetectorNN/uv.lock b/xournalpp_htr/training/WordDetectorNN/uv.lock new file mode 100644 index 0000000000000000000000000000000000000000..1ec13c8159ca9552e28be0dadc75756767d30a64 --- /dev/null +++ b/xournalpp_htr/training/WordDetectorNN/uv.lock @@ -0,0 +1,2947 @@ +version = 1 +revision = 3 +requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'", + "python_full_version == '3.13.*' and platform_python_implementation != 'PyPy'", + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", + "python_full_version < '3.13'", +] + +[[package]] +name = "absl-py" +version = "2.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/10/2a/c93173ffa1b39c1d0395b7e842bbdc62e556ca9d8d3b5572926f3e4ca752/absl_py-2.3.1.tar.gz", hash = "sha256:a97820526f7fbfd2ec1bce83f3f25e3a14840dac0d8e02a0b71cd75db3f77fc9", size = 116588, upload-time = "2025-07-03T09:31:44.05Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/aa/ba0014cc4659328dc818a28827be78e6d97312ab0cb98105a770924dc11e/absl_py-2.3.1-py3-none-any.whl", hash = "sha256:eeecf07f0c2a93ace0772c92e596ace6d3d3996c042b2128459aaae2a76de11d", size = 135811, upload-time = "2025-07-03T09:31:42.253Z" }, +] + +[[package]] +name = "aiofiles" +version = "24.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/03/a88171e277e8caa88a4c77808c20ebb04ba74cc4681bf1e9416c862de237/aiofiles-24.1.0.tar.gz", hash = "sha256:22a075c9e5a3810f0c2e48f3008c94d68c65d763b9b03857924c99e57355166c", size = 30247, upload-time = "2024-06-24T11:02:03.584Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a5/45/30bb92d442636f570cb5651bc661f52b610e2eec3f891a5dc3a4c3667db0/aiofiles-24.1.0-py3-none-any.whl", hash = "sha256:b4ec55f4195e3eb5d7abd1bf7e061763e864dd4954231fb8539a0ef8bb8260e5", size = 15896, upload-time = "2024-06-24T11:02:01.529Z" }, +] + +[[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 = "anyio" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/95/7d/4c1bd541d4dffa1b52bd83fb8527089e097a106fc90b467a7313b105f840/anyio-4.9.0.tar.gz", hash = "sha256:673c0c244e15788651a4ff38710fea9675823028a6f08a5eda409e0c9840a028", size = 190949, upload-time = "2025-03-17T00:02:54.77Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a1/ee/48ca1a7c89ffec8b6a0c5d02b89c305671d5ffd8d3c94acf8b8c408575bb/anyio-4.9.0-py3-none-any.whl", hash = "sha256:9f76d541cad6e36af7beb62e978876f3b41e3e04f2c1fbf0884604c0a9c4d93c", size = 100916, upload-time = "2025-03-17T00:02:52.713Z" }, +] + +[[package]] +name = "appnope" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/35/5d/752690df9ef5b76e169e68d6a129fa6d08a7100ca7f754c89495db3c6019/appnope-0.1.4.tar.gz", hash = "sha256:1de3860566df9caf38f01f86f65e0e13e379af54f9e4bee1e66b48f2efffd1ee", size = 4170, upload-time = "2024-02-06T09:43:11.258Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/29/5ecc3a15d5a33e31b26c11426c45c501e439cb865d0bff96315d86443b78/appnope-0.1.4-py2.py3-none-any.whl", hash = "sha256:502575ee11cd7a28c0205f379b525beefebab9d161b7c964670864014ed7213c", size = 4321, upload-time = "2024-02-06T09:43:09.663Z" }, +] + +[[package]] +name = "argon2-cffi" +version = "25.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "argon2-cffi-bindings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/89/ce5af8a7d472a67cc819d5d998aa8c82c5d860608c4db9f46f1162d7dab9/argon2_cffi-25.1.0.tar.gz", hash = "sha256:694ae5cc8a42f4c4e2bf2ca0e64e51e23a040c6a517a85074683d3959e1346c1", size = 45706, upload-time = "2025-06-03T06:55:32.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4f/d3/a8b22fa575b297cd6e3e3b0155c7e25db170edf1c74783d6a31a2490b8d9/argon2_cffi-25.1.0-py3-none-any.whl", hash = "sha256:fdc8b074db390fccb6eb4a3604ae7231f219aa669a2652e0f20e16ba513d5741", size = 14657, upload-time = "2025-06-03T06:55:30.804Z" }, +] + +[[package]] +name = "argon2-cffi-bindings" +version = "21.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b9/e9/184b8ccce6683b0aa2fbb7ba5683ea4b9c5763f1356347f1312c32e3c66e/argon2-cffi-bindings-21.2.0.tar.gz", hash = "sha256:bb89ceffa6c791807d1305ceb77dbfacc5aa499891d2c55661c6459651fc39e3", size = 1779911, upload-time = "2021-12-01T08:52:55.68Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/13/838ce2620025e9666aa8f686431f67a29052241692a3dd1ae9d3692a89d3/argon2_cffi_bindings-21.2.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:ccb949252cb2ab3a08c02024acb77cfb179492d5701c7cbdbfd776124d4d2367", size = 29658, upload-time = "2021-12-01T09:09:17.016Z" }, + { url = "https://files.pythonhosted.org/packages/b3/02/f7f7bb6b6af6031edb11037639c697b912e1dea2db94d436e681aea2f495/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9524464572e12979364b7d600abf96181d3541da11e23ddf565a32e70bd4dc0d", size = 80583, upload-time = "2021-12-01T09:09:19.546Z" }, + { url = "https://files.pythonhosted.org/packages/ec/f7/378254e6dd7ae6f31fe40c8649eea7d4832a42243acaf0f1fff9083b2bed/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b746dba803a79238e925d9046a63aa26bf86ab2a2fe74ce6b009a1c3f5c8f2ae", size = 86168, upload-time = "2021-12-01T09:09:21.445Z" }, + { url = "https://files.pythonhosted.org/packages/74/f6/4a34a37a98311ed73bb80efe422fed95f2ac25a4cacc5ae1d7ae6a144505/argon2_cffi_bindings-21.2.0-cp36-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:58ed19212051f49a523abb1dbe954337dc82d947fb6e5a0da60f7c8471a8476c", size = 82709, upload-time = "2021-12-01T09:09:18.182Z" }, + { url = "https://files.pythonhosted.org/packages/74/2b/73d767bfdaab25484f7e7901379d5f8793cccbb86c6e0cbc4c1b96f63896/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_aarch64.whl", hash = "sha256:bd46088725ef7f58b5a1ef7ca06647ebaf0eb4baff7d1d0d177c6cc8744abd86", size = 83613, upload-time = "2021-12-01T09:09:22.741Z" }, + { url = "https://files.pythonhosted.org/packages/4f/fd/37f86deef67ff57c76f137a67181949c2d408077e2e3dd70c6c42912c9bf/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_i686.whl", hash = "sha256:8cd69c07dd875537a824deec19f978e0f2078fdda07fd5c42ac29668dda5f40f", size = 84583, upload-time = "2021-12-01T09:09:24.177Z" }, + { url = "https://files.pythonhosted.org/packages/6f/52/5a60085a3dae8fded8327a4f564223029f5f54b0cb0455a31131b5363a01/argon2_cffi_bindings-21.2.0-cp36-abi3-musllinux_1_1_x86_64.whl", hash = "sha256:f1152ac548bd5b8bcecfb0b0371f082037e47128653df2e8ba6e914d384f3c3e", size = 88475, upload-time = "2021-12-01T09:09:26.673Z" }, + { url = "https://files.pythonhosted.org/packages/8b/95/143cd64feb24a15fa4b189a3e1e7efbaeeb00f39a51e99b26fc62fbacabd/argon2_cffi_bindings-21.2.0-cp36-abi3-win32.whl", hash = "sha256:603ca0aba86b1349b147cab91ae970c63118a0f30444d4bc80355937c950c082", size = 27698, upload-time = "2021-12-01T09:09:27.87Z" }, + { url = "https://files.pythonhosted.org/packages/37/2c/e34e47c7dee97ba6f01a6203e0383e15b60fb85d78ac9a15cd066f6fe28b/argon2_cffi_bindings-21.2.0-cp36-abi3-win_amd64.whl", hash = "sha256:b2ef1c30440dbbcba7a5dc3e319408b59676e2e039e2ae11a8775ecf482b192f", size = 30817, upload-time = "2021-12-01T09:09:30.267Z" }, + { url = "https://files.pythonhosted.org/packages/5a/e4/bf8034d25edaa495da3c8a3405627d2e35758e44ff6eaa7948092646fdcc/argon2_cffi_bindings-21.2.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:e415e3f62c8d124ee16018e491a009937f8cf7ebf5eb430ffc5de21b900dad93", size = 53104, upload-time = "2021-12-01T09:09:31.335Z" }, +] + +[[package]] +name = "arrow" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dateutil" }, + { name = "types-python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2e/00/0f6e8fcdb23ea632c866620cc872729ff43ed91d284c866b515c6342b173/arrow-1.3.0.tar.gz", hash = "sha256:d4540617648cb5f895730f1ad8c82a65f2dad0166f57b75f3ca54759c4d67a85", size = 131960, upload-time = "2023-09-30T22:11:18.25Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/ed/e97229a566617f2ae958a6b13e7cc0f585470eac730a73e9e82c32a3cdd2/arrow-1.3.0-py3-none-any.whl", hash = "sha256:c728b120ebc00eb84e01882a6f5e7927a53960aa990ce7dd2b10f39005a67f80", size = 66419, upload-time = "2023-09-30T22:11:16.072Z" }, +] + +[[package]] +name = "asttokens" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4a/e7/82da0a03e7ba5141f05cce0d302e6eed121ae055e0456ca228bf693984bc/asttokens-3.0.0.tar.gz", hash = "sha256:0dcd8baa8d62b0c1d118b399b2ddba3c4aff271d0d7a9e0d4c1681c79035bbc7", size = 61978, upload-time = "2024-11-30T04:30:14.439Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/25/8a/c46dcc25341b5bce5472c718902eb3d38600a903b14fa6aeecef3f21a46f/asttokens-3.0.0-py3-none-any.whl", hash = "sha256:e3078351a059199dd5138cb1c706e6430c05eff2ff136af5eb4790f9d28932e2", size = 26918, upload-time = "2024-11-30T04:30:10.946Z" }, +] + +[[package]] +name = "async-lru" +version = "2.0.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b2/4d/71ec4d3939dc755264f680f6c2b4906423a304c3d18e96853f0a595dfe97/async_lru-2.0.5.tar.gz", hash = "sha256:481d52ccdd27275f42c43a928b4a50c3bfb2d67af4e78b170e3e0bb39c66e5bb", size = 10380, upload-time = "2025-03-16T17:25:36.919Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/03/49/d10027df9fce941cb8184e78a02857af36360d33e1721df81c5ed2179a1a/async_lru-2.0.5-py3-none-any.whl", hash = "sha256:ab95404d8d2605310d345932697371a5f40def0487c03d6d0ad9138de52c9943", size = 6069, upload-time = "2025-03-16T17:25:35.422Z" }, +] + +[[package]] +name = "attrs" +version = "25.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5a/b0/1367933a8532ee6ff8d63537de4f1177af4bff9f3e829baf7331f595bb24/attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b", size = 812032, upload-time = "2025-03-13T11:10:22.779Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/06/bb80f5f86020c4551da315d78b3ab75e8228f89f0162f2c3a819e407941a/attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3", size = 63815, upload-time = "2025-03-13T11:10:21.14Z" }, +] + +[[package]] +name = "audioop-lts" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/53/946db57842a50b2da2e0c1e34bd37f36f5aadba1a929a3971c5d7841dbca/audioop_lts-0.2.2.tar.gz", hash = "sha256:64d0c62d88e67b98a1a5e71987b7aa7b5bcffc7dcee65b635823dbdd0a8dbbd0", size = 30686, upload-time = "2025-08-05T16:43:17.409Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/de/d4/94d277ca941de5a507b07f0b592f199c22454eeaec8f008a286b3fbbacd6/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_universal2.whl", hash = "sha256:fd3d4602dc64914d462924a08c1a9816435a2155d74f325853c1f1ac3b2d9800", size = 46523, upload-time = "2025-08-05T16:42:20.836Z" }, + { url = "https://files.pythonhosted.org/packages/f8/5a/656d1c2da4b555920ce4177167bfeb8623d98765594af59702c8873f60ec/audioop_lts-0.2.2-cp313-abi3-macosx_10_13_x86_64.whl", hash = "sha256:550c114a8df0aafe9a05442a1162dfc8fec37e9af1d625ae6060fed6e756f303", size = 27455, upload-time = "2025-08-05T16:42:22.283Z" }, + { url = "https://files.pythonhosted.org/packages/1b/83/ea581e364ce7b0d41456fb79d6ee0ad482beda61faf0cab20cbd4c63a541/audioop_lts-0.2.2-cp313-abi3-macosx_11_0_arm64.whl", hash = "sha256:9a13dc409f2564de15dd68be65b462ba0dde01b19663720c68c1140c782d1d75", size = 26997, upload-time = "2025-08-05T16:42:23.849Z" }, + { url = "https://files.pythonhosted.org/packages/b8/3b/e8964210b5e216e5041593b7d33e97ee65967f17c282e8510d19c666dab4/audioop_lts-0.2.2-cp313-abi3-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:51c916108c56aa6e426ce611946f901badac950ee2ddaf302b7ed35d9958970d", size = 85844, upload-time = "2025-08-05T16:42:25.208Z" }, + { url = "https://files.pythonhosted.org/packages/c7/2e/0a1c52faf10d51def20531a59ce4c706cb7952323b11709e10de324d6493/audioop_lts-0.2.2-cp313-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:47eba38322370347b1c47024defbd36374a211e8dd5b0dcbce7b34fdb6f8847b", size = 85056, upload-time = "2025-08-05T16:42:26.559Z" }, + { url = "https://files.pythonhosted.org/packages/75/e8/cd95eef479656cb75ab05dfece8c1f8c395d17a7c651d88f8e6e291a63ab/audioop_lts-0.2.2-cp313-abi3-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ba7c3a7e5f23e215cb271516197030c32aef2e754252c4c70a50aaff7031a2c8", size = 93892, upload-time = "2025-08-05T16:42:27.902Z" }, + { url = "https://files.pythonhosted.org/packages/5c/1e/a0c42570b74f83efa5cca34905b3eef03f7ab09fe5637015df538a7f3345/audioop_lts-0.2.2-cp313-abi3-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:def246fe9e180626731b26e89816e79aae2276f825420a07b4a647abaa84becc", size = 96660, upload-time = "2025-08-05T16:42:28.9Z" }, + { url = "https://files.pythonhosted.org/packages/50/d5/8a0ae607ca07dbb34027bac8db805498ee7bfecc05fd2c148cc1ed7646e7/audioop_lts-0.2.2-cp313-abi3-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e160bf9df356d841bb6c180eeeea1834085464626dc1b68fa4e1d59070affdc3", size = 79143, upload-time = "2025-08-05T16:42:29.929Z" }, + { url = "https://files.pythonhosted.org/packages/12/17/0d28c46179e7910bfb0bb62760ccb33edb5de973052cb2230b662c14ca2e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:4b4cd51a57b698b2d06cb9993b7ac8dfe89a3b2878e96bc7948e9f19ff51dba6", size = 84313, upload-time = "2025-08-05T16:42:30.949Z" }, + { url = "https://files.pythonhosted.org/packages/84/ba/bd5d3806641564f2024e97ca98ea8f8811d4e01d9b9f9831474bc9e14f9e/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_ppc64le.whl", hash = "sha256:4a53aa7c16a60a6857e6b0b165261436396ef7293f8b5c9c828a3a203147ed4a", size = 93044, upload-time = "2025-08-05T16:42:31.959Z" }, + { url = "https://files.pythonhosted.org/packages/f9/5e/435ce8d5642f1f7679540d1e73c1c42d933331c0976eb397d1717d7f01a3/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_riscv64.whl", hash = "sha256:3fc38008969796f0f689f1453722a0f463da1b8a6fbee11987830bfbb664f623", size = 78766, upload-time = "2025-08-05T16:42:33.302Z" }, + { url = "https://files.pythonhosted.org/packages/ae/3b/b909e76b606cbfd53875693ec8c156e93e15a1366a012f0b7e4fb52d3c34/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_s390x.whl", hash = "sha256:15ab25dd3e620790f40e9ead897f91e79c0d3ce65fe193c8ed6c26cffdd24be7", size = 87640, upload-time = "2025-08-05T16:42:34.854Z" }, + { url = "https://files.pythonhosted.org/packages/30/e7/8f1603b4572d79b775f2140d7952f200f5e6c62904585d08a01f0a70393a/audioop_lts-0.2.2-cp313-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:03f061a1915538fd96272bac9551841859dbb2e3bf73ebe4a23ef043766f5449", size = 86052, upload-time = "2025-08-05T16:42:35.839Z" }, + { url = "https://files.pythonhosted.org/packages/b5/96/c37846df657ccdda62ba1ae2b6534fa90e2e1b1742ca8dcf8ebd38c53801/audioop_lts-0.2.2-cp313-abi3-win32.whl", hash = "sha256:3bcddaaf6cc5935a300a8387c99f7a7fbbe212a11568ec6cf6e4bc458c048636", size = 26185, upload-time = "2025-08-05T16:42:37.04Z" }, + { url = "https://files.pythonhosted.org/packages/34/a5/9d78fdb5b844a83da8a71226c7bdae7cc638861085fff7a1d707cb4823fa/audioop_lts-0.2.2-cp313-abi3-win_amd64.whl", hash = "sha256:a2c2a947fae7d1062ef08c4e369e0ba2086049a5e598fda41122535557012e9e", size = 30503, upload-time = "2025-08-05T16:42:38.427Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/20d8fde083123e90c61b51afb547bb0ea7e77bab50d98c0ab243d02a0e43/audioop_lts-0.2.2-cp313-abi3-win_arm64.whl", hash = "sha256:5f93a5db13927a37d2d09637ccca4b2b6b48c19cd9eda7b17a2e9f77edee6a6f", size = 24173, upload-time = "2025-08-05T16:42:39.704Z" }, + { url = "https://files.pythonhosted.org/packages/58/a7/0a764f77b5c4ac58dc13c01a580f5d32ae8c74c92020b961556a43e26d02/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:73f80bf4cd5d2ca7814da30a120de1f9408ee0619cc75da87d0641273d202a09", size = 47096, upload-time = "2025-08-05T16:42:40.684Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ed/ebebedde1a18848b085ad0fa54b66ceb95f1f94a3fc04f1cd1b5ccb0ed42/audioop_lts-0.2.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:106753a83a25ee4d6f473f2be6b0966fc1c9af7e0017192f5531a3e7463dce58", size = 27748, upload-time = "2025-08-05T16:42:41.992Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6e/11ca8c21af79f15dbb1c7f8017952ee8c810c438ce4e2b25638dfef2b02c/audioop_lts-0.2.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:fbdd522624141e40948ab3e8cdae6e04c748d78710e9f0f8d4dae2750831de19", size = 27329, upload-time = "2025-08-05T16:42:42.987Z" }, + { url = "https://files.pythonhosted.org/packages/84/52/0022f93d56d85eec5da6b9da6a958a1ef09e80c39f2cc0a590c6af81dcbb/audioop_lts-0.2.2-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:143fad0311e8209ece30a8dbddab3b65ab419cbe8c0dde6e8828da25999be911", size = 92407, upload-time = "2025-08-05T16:42:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/87/1d/48a889855e67be8718adbc7a01f3c01d5743c325453a5e81cf3717664aad/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfbbc74ec68a0fd08cfec1f4b5e8cca3d3cd7de5501b01c4b5d209995033cde9", size = 91811, upload-time = "2025-08-05T16:42:45.325Z" }, + { url = "https://files.pythonhosted.org/packages/98/a6/94b7213190e8077547ffae75e13ed05edc488653c85aa5c41472c297d295/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cfcac6aa6f42397471e4943e0feb2244549db5c5d01efcd02725b96af417f3fe", size = 100470, upload-time = "2025-08-05T16:42:46.468Z" }, + { url = "https://files.pythonhosted.org/packages/e9/e9/78450d7cb921ede0cfc33426d3a8023a3bda755883c95c868ee36db8d48d/audioop_lts-0.2.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:752d76472d9804ac60f0078c79cdae8b956f293177acd2316cd1e15149aee132", size = 103878, upload-time = "2025-08-05T16:42:47.576Z" }, + { url = "https://files.pythonhosted.org/packages/4f/e2/cd5439aad4f3e34ae1ee852025dc6aa8f67a82b97641e390bf7bd9891d3e/audioop_lts-0.2.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:83c381767e2cc10e93e40281a04852facc4cd9334550e0f392f72d1c0a9c5753", size = 84867, upload-time = "2025-08-05T16:42:49.003Z" }, + { url = "https://files.pythonhosted.org/packages/68/4b/9d853e9076c43ebba0d411e8d2aa19061083349ac695a7d082540bad64d0/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c0022283e9556e0f3643b7c3c03f05063ca72b3063291834cca43234f20c60bb", size = 90001, upload-time = "2025-08-05T16:42:50.038Z" }, + { url = "https://files.pythonhosted.org/packages/58/26/4bae7f9d2f116ed5593989d0e521d679b0d583973d203384679323d8fa85/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:a2d4f1513d63c795e82948e1305f31a6d530626e5f9f2605408b300ae6095093", size = 99046, upload-time = "2025-08-05T16:42:51.111Z" }, + { url = "https://files.pythonhosted.org/packages/b2/67/a9f4fb3e250dda9e9046f8866e9fa7d52664f8985e445c6b4ad6dfb55641/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:c9c8e68d8b4a56fda8c025e538e639f8c5953f5073886b596c93ec9b620055e7", size = 84788, upload-time = "2025-08-05T16:42:52.198Z" }, + { url = "https://files.pythonhosted.org/packages/70/f7/3de86562db0121956148bcb0fe5b506615e3bcf6e63c4357a612b910765a/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:96f19de485a2925314f5020e85911fb447ff5fbef56e8c7c6927851b95533a1c", size = 94472, upload-time = "2025-08-05T16:42:53.59Z" }, + { url = "https://files.pythonhosted.org/packages/f1/32/fd772bf9078ae1001207d2df1eef3da05bea611a87dd0e8217989b2848fa/audioop_lts-0.2.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:e541c3ef484852ef36545f66209444c48b28661e864ccadb29daddb6a4b8e5f5", size = 92279, upload-time = "2025-08-05T16:42:54.632Z" }, + { url = "https://files.pythonhosted.org/packages/4f/41/affea7181592ab0ab560044632571a38edaf9130b84928177823fbf3176a/audioop_lts-0.2.2-cp313-cp313t-win32.whl", hash = "sha256:d5e73fa573e273e4f2e5ff96f9043858a5e9311e94ffefd88a3186a910c70917", size = 26568, upload-time = "2025-08-05T16:42:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/28/2b/0372842877016641db8fc54d5c88596b542eec2f8f6c20a36fb6612bf9ee/audioop_lts-0.2.2-cp313-cp313t-win_amd64.whl", hash = "sha256:9191d68659eda01e448188f60364c7763a7ca6653ed3f87ebb165822153a8547", size = 30942, upload-time = "2025-08-05T16:42:56.674Z" }, + { url = "https://files.pythonhosted.org/packages/ee/ca/baf2b9cc7e96c179bb4a54f30fcd83e6ecb340031bde68f486403f943768/audioop_lts-0.2.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c174e322bb5783c099aaf87faeb240c8d210686b04bd61dfd05a8e5a83d88969", size = 24603, upload-time = "2025-08-05T16:42:57.571Z" }, + { url = "https://files.pythonhosted.org/packages/5c/73/413b5a2804091e2c7d5def1d618e4837f1cb82464e230f827226278556b7/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:f9ee9b52f5f857fbaf9d605a360884f034c92c1c23021fb90b2e39b8e64bede6", size = 47104, upload-time = "2025-08-05T16:42:58.518Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/daa3308dc6593944410c2c68306a5e217f5c05b70a12e70228e7dd42dc5c/audioop_lts-0.2.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:49ee1a41738a23e98d98b937a0638357a2477bc99e61b0f768a8f654f45d9b7a", size = 27754, upload-time = "2025-08-05T16:43:00.132Z" }, + { url = "https://files.pythonhosted.org/packages/4e/86/c2e0f627168fcf61781a8f72cab06b228fe1da4b9fa4ab39cfb791b5836b/audioop_lts-0.2.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5b00be98ccd0fc123dcfad31d50030d25fcf31488cde9e61692029cd7394733b", size = 27332, upload-time = "2025-08-05T16:43:01.666Z" }, + { url = "https://files.pythonhosted.org/packages/c7/bd/35dce665255434f54e5307de39e31912a6f902d4572da7c37582809de14f/audioop_lts-0.2.2-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:a6d2e0f9f7a69403e388894d4ca5ada5c47230716a03f2847cfc7bd1ecb589d6", size = 92396, upload-time = "2025-08-05T16:43:02.991Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d2/deeb9f51def1437b3afa35aeb729d577c04bcd89394cb56f9239a9f50b6f/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9b0b8a03ef474f56d1a842af1a2e01398b8f7654009823c6d9e0ecff4d5cfbf", size = 91811, upload-time = "2025-08-05T16:43:04.096Z" }, + { url = "https://files.pythonhosted.org/packages/76/3b/09f8b35b227cee28cc8231e296a82759ed80c1a08e349811d69773c48426/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2b267b70747d82125f1a021506565bdc5609a2b24bcb4773c16d79d2bb260bbd", size = 100483, upload-time = "2025-08-05T16:43:05.085Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/05b48a935cf3b130c248bfdbdea71ce6437f5394ee8533e0edd7cfd93d5e/audioop_lts-0.2.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0337d658f9b81f4cd0fdb1f47635070cc084871a3d4646d9de74fdf4e7c3d24a", size = 103885, upload-time = "2025-08-05T16:43:06.197Z" }, + { url = "https://files.pythonhosted.org/packages/83/80/186b7fce6d35b68d3d739f228dc31d60b3412105854edb975aa155a58339/audioop_lts-0.2.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:167d3b62586faef8b6b2275c3218796b12621a60e43f7e9d5845d627b9c9b80e", size = 84899, upload-time = "2025-08-05T16:43:07.291Z" }, + { url = "https://files.pythonhosted.org/packages/49/89/c78cc5ac6cb5828f17514fb12966e299c850bc885e80f8ad94e38d450886/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:0d9385e96f9f6da847f4d571ce3cb15b5091140edf3db97276872647ce37efd7", size = 89998, upload-time = "2025-08-05T16:43:08.335Z" }, + { url = "https://files.pythonhosted.org/packages/4c/4b/6401888d0c010e586c2ca50fce4c903d70a6bb55928b16cfbdfd957a13da/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:48159d96962674eccdca9a3df280e864e8ac75e40a577cc97c5c42667ffabfc5", size = 99046, upload-time = "2025-08-05T16:43:09.367Z" }, + { url = "https://files.pythonhosted.org/packages/de/f8/c874ca9bb447dae0e2ef2e231f6c4c2b0c39e31ae684d2420b0f9e97ee68/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:8fefe5868cd082db1186f2837d64cfbfa78b548ea0d0543e9b28935ccce81ce9", size = 84843, upload-time = "2025-08-05T16:43:10.749Z" }, + { url = "https://files.pythonhosted.org/packages/3e/c0/0323e66f3daebc13fd46b36b30c3be47e3fc4257eae44f1e77eb828c703f/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:58cf54380c3884fb49fdd37dfb7a772632b6701d28edd3e2904743c5e1773602", size = 94490, upload-time = "2025-08-05T16:43:12.131Z" }, + { url = "https://files.pythonhosted.org/packages/98/6b/acc7734ac02d95ab791c10c3f17ffa3584ccb9ac5c18fd771c638ed6d1f5/audioop_lts-0.2.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:088327f00488cdeed296edd9215ca159f3a5a5034741465789cad403fcf4bec0", size = 92297, upload-time = "2025-08-05T16:43:13.139Z" }, + { url = "https://files.pythonhosted.org/packages/13/c3/c3dc3f564ce6877ecd2a05f8d751b9b27a8c320c2533a98b0c86349778d0/audioop_lts-0.2.2-cp314-cp314t-win32.whl", hash = "sha256:068aa17a38b4e0e7de771c62c60bbca2455924b67a8814f3b0dee92b5820c0b3", size = 27331, upload-time = "2025-08-05T16:43:14.19Z" }, + { url = "https://files.pythonhosted.org/packages/72/bb/b4608537e9ffcb86449091939d52d24a055216a36a8bf66b936af8c3e7ac/audioop_lts-0.2.2-cp314-cp314t-win_amd64.whl", hash = "sha256:a5bf613e96f49712073de86f20dbdd4014ca18efd4d34ed18c75bd808337851b", size = 31697, upload-time = "2025-08-05T16:43:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/f6/22/91616fe707a5c5510de2cac9b046a30defe7007ba8a0c04f9c08f27df312/audioop_lts-0.2.2-cp314-cp314t-win_arm64.whl", hash = "sha256:b492c3b040153e68b9fdaff5913305aaaba5bb433d8a7f73d5cf6a64ed3cc1dd", size = 25206, upload-time = "2025-08-05T16:43:16.444Z" }, +] + +[[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 = "beautifulsoup4" +version = "4.13.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "soupsieve" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d8/e4/0c4c39e18fd76d6a628d4dd8da40543d136ce2d1752bd6eeeab0791f4d6b/beautifulsoup4-4.13.4.tar.gz", hash = "sha256:dbb3c4e1ceae6aefebdaf2423247260cd062430a410e38c66f2baa50a8437195", size = 621067, upload-time = "2025-04-15T17:05:13.836Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/50/cd/30110dc0ffcf3b131156077b90e9f60ed75711223f306da4db08eff8403b/beautifulsoup4-4.13.4-py3-none-any.whl", hash = "sha256:9bbbb14bfde9d79f38b8cd5f8c7c85f4b8f2523190ebed90e950a8dea4cb1c4b", size = 187285, upload-time = "2025-04-15T17:05:12.221Z" }, +] + +[[package]] +name = "bleach" +version = "6.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/76/9a/0e33f5054c54d349ea62c277191c020c2d6ef1d65ab2cb1993f91ec846d1/bleach-6.2.0.tar.gz", hash = "sha256:123e894118b8a599fd80d3ec1a6d4cc7ce4e5882b1317a7e1ba69b56e95f991f", size = 203083, upload-time = "2024-10-29T18:30:40.477Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fc/55/96142937f66150805c25c4d0f31ee4132fd33497753400734f9dfdcbdc66/bleach-6.2.0-py3-none-any.whl", hash = "sha256:117d9c6097a7c3d22fd578fcd8d35ff1e125df6736f554da4e432fdd63f31e5e", size = 163406, upload-time = "2024-10-29T18:30:38.186Z" }, +] + +[package.optional-dependencies] +css = [ + { name = "tinycss2" }, +] + +[[package]] +name = "brotli" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2f/c2/f9e977608bdf958650638c3f1e28f85a1b075f075ebbe77db8555463787b/Brotli-1.1.0.tar.gz", hash = "sha256:81de08ac11bcb85841e440c13611c00b67d3bf82698314928d0b676362546724", size = 7372270, upload-time = "2023-09-07T14:05:41.643Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/d0/5373ae13b93fe00095a58efcbce837fd470ca39f703a235d2a999baadfbc/Brotli-1.1.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:32d95b80260d79926f5fab3c41701dbb818fde1c9da590e77e571eefd14abe28", size = 815693, upload-time = "2024-10-18T12:32:23.824Z" }, + { url = "https://files.pythonhosted.org/packages/8e/48/f6e1cdf86751300c288c1459724bfa6917a80e30dbfc326f92cea5d3683a/Brotli-1.1.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b760c65308ff1e462f65d69c12e4ae085cff3b332d894637f6273a12a482d09f", size = 422489, upload-time = "2024-10-18T12:32:25.641Z" }, + { url = "https://files.pythonhosted.org/packages/06/88/564958cedce636d0f1bed313381dfc4b4e3d3f6015a63dae6146e1b8c65c/Brotli-1.1.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:316cc9b17edf613ac76b1f1f305d2a748f1b976b033b049a6ecdfd5612c70409", size = 873081, upload-time = "2023-09-07T14:03:57.967Z" }, + { url = "https://files.pythonhosted.org/packages/58/79/b7026a8bb65da9a6bb7d14329fd2bd48d2b7f86d7329d5cc8ddc6a90526f/Brotli-1.1.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:caf9ee9a5775f3111642d33b86237b05808dafcd6268faa492250e9b78046eb2", size = 446244, upload-time = "2023-09-07T14:03:59.319Z" }, + { url = "https://files.pythonhosted.org/packages/e5/18/c18c32ecea41b6c0004e15606e274006366fe19436b6adccc1ae7b2e50c2/Brotli-1.1.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:70051525001750221daa10907c77830bc889cb6d865cc0b813d9db7fefc21451", size = 2906505, upload-time = "2023-09-07T14:04:01.327Z" }, + { url = "https://files.pythonhosted.org/packages/08/c8/69ec0496b1ada7569b62d85893d928e865df29b90736558d6c98c2031208/Brotli-1.1.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7f4bf76817c14aa98cc6697ac02f3972cb8c3da93e9ef16b9c66573a68014f91", size = 2944152, upload-time = "2023-09-07T14:04:03.033Z" }, + { url = "https://files.pythonhosted.org/packages/ab/fb/0517cea182219d6768113a38167ef6d4eb157a033178cc938033a552ed6d/Brotli-1.1.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d0c5516f0aed654134a2fc936325cc2e642f8a0e096d075209672eb321cff408", size = 2919252, upload-time = "2023-09-07T14:04:04.675Z" }, + { url = "https://files.pythonhosted.org/packages/c7/53/73a3431662e33ae61a5c80b1b9d2d18f58dfa910ae8dd696e57d39f1a2f5/Brotli-1.1.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6c3020404e0b5eefd7c9485ccf8393cfb75ec38ce75586e046573c9dc29967a0", size = 2845955, upload-time = "2023-09-07T14:04:06.585Z" }, + { url = "https://files.pythonhosted.org/packages/55/ac/bd280708d9c5ebdbf9de01459e625a3e3803cce0784f47d633562cf40e83/Brotli-1.1.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:4ed11165dd45ce798d99a136808a794a748d5dc38511303239d4e2363c0695dc", size = 2914304, upload-time = "2023-09-07T14:04:08.668Z" }, + { url = "https://files.pythonhosted.org/packages/76/58/5c391b41ecfc4527d2cc3350719b02e87cb424ef8ba2023fb662f9bf743c/Brotli-1.1.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:4093c631e96fdd49e0377a9c167bfd75b6d0bad2ace734c6eb20b348bc3ea180", size = 2814452, upload-time = "2023-09-07T14:04:10.736Z" }, + { url = "https://files.pythonhosted.org/packages/c7/4e/91b8256dfe99c407f174924b65a01f5305e303f486cc7a2e8a5d43c8bec3/Brotli-1.1.0-cp312-cp312-musllinux_1_1_ppc64le.whl", hash = "sha256:7e4c4629ddad63006efa0ef968c8e4751c5868ff0b1c5c40f76524e894c50248", size = 2938751, upload-time = "2023-09-07T14:04:12.875Z" }, + { url = "https://files.pythonhosted.org/packages/5a/a6/e2a39a5d3b412938362bbbeba5af904092bf3f95b867b4a3eb856104074e/Brotli-1.1.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:861bf317735688269936f755fa136a99d1ed526883859f86e41a5d43c61d8966", size = 2933757, upload-time = "2023-09-07T14:04:14.551Z" }, + { url = "https://files.pythonhosted.org/packages/13/f0/358354786280a509482e0e77c1a5459e439766597d280f28cb097642fc26/Brotli-1.1.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:87a3044c3a35055527ac75e419dfa9f4f3667a1e887ee80360589eb8c90aabb9", size = 2936146, upload-time = "2024-10-18T12:32:27.257Z" }, + { url = "https://files.pythonhosted.org/packages/80/f7/daf538c1060d3a88266b80ecc1d1c98b79553b3f117a485653f17070ea2a/Brotli-1.1.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c5529b34c1c9d937168297f2c1fde7ebe9ebdd5e121297ff9c043bdb2ae3d6fb", size = 2848055, upload-time = "2024-10-18T12:32:29.376Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0eaa0585c4077d3c2d1edf322d8e97aabf317941d3a72d7b3ad8bce004b0/Brotli-1.1.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:ca63e1890ede90b2e4454f9a65135a4d387a4585ff8282bb72964fab893f2111", size = 3035102, upload-time = "2024-10-18T12:32:31.371Z" }, + { url = "https://files.pythonhosted.org/packages/d8/63/1c1585b2aa554fe6dbce30f0c18bdbc877fa9a1bf5ff17677d9cca0ac122/Brotli-1.1.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:e79e6520141d792237c70bcd7a3b122d00f2613769ae0cb61c52e89fd3443839", size = 2930029, upload-time = "2024-10-18T12:32:33.293Z" }, + { url = "https://files.pythonhosted.org/packages/5f/3b/4e3fd1893eb3bbfef8e5a80d4508bec17a57bb92d586c85c12d28666bb13/Brotli-1.1.0-cp312-cp312-win32.whl", hash = "sha256:5f4d5ea15c9382135076d2fb28dde923352fe02951e66935a9efaac8f10e81b0", size = 333276, upload-time = "2023-09-07T14:04:16.49Z" }, + { url = "https://files.pythonhosted.org/packages/3d/d5/942051b45a9e883b5b6e98c041698b1eb2012d25e5948c58d6bf85b1bb43/Brotli-1.1.0-cp312-cp312-win_amd64.whl", hash = "sha256:906bc3a79de8c4ae5b86d3d75a8b77e44404b0f4261714306e3ad248d8ab0951", size = 357255, upload-time = "2023-09-07T14:04:17.83Z" }, + { url = "https://files.pythonhosted.org/packages/0a/9f/fb37bb8ffc52a8da37b1c03c459a8cd55df7a57bdccd8831d500e994a0ca/Brotli-1.1.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8bf32b98b75c13ec7cf774164172683d6e7891088f6316e54425fde1efc276d5", size = 815681, upload-time = "2024-10-18T12:32:34.942Z" }, + { url = "https://files.pythonhosted.org/packages/06/b3/dbd332a988586fefb0aa49c779f59f47cae76855c2d00f450364bb574cac/Brotli-1.1.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7bc37c4d6b87fb1017ea28c9508b36bbcb0c3d18b4260fcdf08b200c74a6aee8", size = 422475, upload-time = "2024-10-18T12:32:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/bb/80/6aaddc2f63dbcf2d93c2d204e49c11a9ec93a8c7c63261e2b4bd35198283/Brotli-1.1.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3c0ef38c7a7014ffac184db9e04debe495d317cc9c6fb10071f7fefd93100a4f", size = 2906173, upload-time = "2024-10-18T12:32:37.978Z" }, + { url = "https://files.pythonhosted.org/packages/ea/1d/e6ca79c96ff5b641df6097d299347507d39a9604bde8915e76bf026d6c77/Brotli-1.1.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:91d7cc2a76b5567591d12c01f019dd7afce6ba8cba6571187e21e2fc418ae648", size = 2943803, upload-time = "2024-10-18T12:32:39.606Z" }, + { url = "https://files.pythonhosted.org/packages/ac/a3/d98d2472e0130b7dd3acdbb7f390d478123dbf62b7d32bda5c830a96116d/Brotli-1.1.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a93dde851926f4f2678e704fadeb39e16c35d8baebd5252c9fd94ce8ce68c4a0", size = 2918946, upload-time = "2024-10-18T12:32:41.679Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a5/c69e6d272aee3e1423ed005d8915a7eaa0384c7de503da987f2d224d0721/Brotli-1.1.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f0db75f47be8b8abc8d9e31bc7aad0547ca26f24a54e6fd10231d623f183d089", size = 2845707, upload-time = "2024-10-18T12:32:43.478Z" }, + { url = "https://files.pythonhosted.org/packages/58/9f/4149d38b52725afa39067350696c09526de0125ebfbaab5acc5af28b42ea/Brotli-1.1.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6967ced6730aed543b8673008b5a391c3b1076d834ca438bbd70635c73775368", size = 2936231, upload-time = "2024-10-18T12:32:45.224Z" }, + { url = "https://files.pythonhosted.org/packages/5a/5a/145de884285611838a16bebfdb060c231c52b8f84dfbe52b852a15780386/Brotli-1.1.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:7eedaa5d036d9336c95915035fb57422054014ebdeb6f3b42eac809928e40d0c", size = 2848157, upload-time = "2024-10-18T12:32:46.894Z" }, + { url = "https://files.pythonhosted.org/packages/50/ae/408b6bfb8525dadebd3b3dd5b19d631da4f7d46420321db44cd99dcf2f2c/Brotli-1.1.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d487f5432bf35b60ed625d7e1b448e2dc855422e87469e3f450aa5552b0eb284", size = 3035122, upload-time = "2024-10-18T12:32:48.844Z" }, + { url = "https://files.pythonhosted.org/packages/af/85/a94e5cfaa0ca449d8f91c3d6f78313ebf919a0dbd55a100c711c6e9655bc/Brotli-1.1.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:832436e59afb93e1836081a20f324cb185836c617659b07b129141a8426973c7", size = 2930206, upload-time = "2024-10-18T12:32:51.198Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f0/a61d9262cd01351df22e57ad7c34f66794709acab13f34be2675f45bf89d/Brotli-1.1.0-cp313-cp313-win32.whl", hash = "sha256:43395e90523f9c23a3d5bdf004733246fba087f2948f87ab28015f12359ca6a0", size = 333804, upload-time = "2024-10-18T12:32:52.661Z" }, + { url = "https://files.pythonhosted.org/packages/7e/c1/ec214e9c94000d1c1974ec67ced1c970c148aa6b8d8373066123fc3dbf06/Brotli-1.1.0-cp313-cp313-win_amd64.whl", hash = "sha256:9011560a466d2eb3f5a6e4929cf4a09be405c64154e12df0dd72713f6500e32b", size = 358517, upload-time = "2024-10-18T12:32:54.066Z" }, +] + +[[package]] +name = "certifi" +version = "2025.6.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/73/f7/f14b46d4bcd21092d7d3ccef689615220d8a08fb25e564b65d20738e672e/certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b", size = 158753, upload-time = "2025-06-15T02:45:51.329Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/84/ae/320161bd181fc06471eed047ecce67b693fd7515b16d495d8932db763426/certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057", size = 157650, upload-time = "2025-06-15T02:45:49.977Z" }, +] + +[[package]] +name = "cffi" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pycparser" }, +] +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/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/cc/b6/db007700f67d151abadf508cbfd6a1884f57eab90b1bb985c4c8c02b0f28/cffi-1.17.1-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1257bdabf294dceb59f5e70c64a3e2f462c30c7ad68092d01bbbfb1c16b1ba36", size = 454803, upload-time = "2024-09-04T20:44:15.231Z" }, + { url = "https://files.pythonhosted.org/packages/1a/df/f8d151540d8c200eb1c6fba8cd0dfd40904f1b0682ea705c36e6c2e97ab3/cffi-1.17.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:da95af8214998d77a98cc14e3a3bd00aa191526343078b530ceb0bd710fb48a5", size = 478850, upload-time = "2024-09-04T20:44:17.188Z" }, + { url = "https://files.pythonhosted.org/packages/28/c0/b31116332a547fd2677ae5b78a2ef662dfc8023d67f41b2a83f7c2aa78b1/cffi-1.17.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d63afe322132c194cf832bfec0dc69a99fb9bb6bbd550f161a49e9e855cc78ff", size = 485729, upload-time = "2024-09-04T20:44:18.688Z" }, + { url = "https://files.pythonhosted.org/packages/91/2b/9a1ddfa5c7f13cab007a2c9cc295b70fbbda7cb10a286aa6810338e60ea1/cffi-1.17.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f79fc4fc25f1c8698ff97788206bb3c2598949bfe0fef03d299eb1b5356ada99", size = 471256, upload-time = "2024-09-04T20:44:20.248Z" }, + { url = "https://files.pythonhosted.org/packages/b2/d5/da47df7004cb17e4955df6a43d14b3b4ae77737dff8bf7f8f333196717bf/cffi-1.17.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b62ce867176a75d03a665bad002af8e6d54644fad99a3c70905c543130e39d93", size = 479424, upload-time = "2024-09-04T20:44:21.673Z" }, + { url = "https://files.pythonhosted.org/packages/0b/ac/2a28bcf513e93a219c8a4e8e125534f4f6db03e3179ba1c45e949b76212c/cffi-1.17.1-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:386c8bf53c502fff58903061338ce4f4950cbdcb23e2902d86c0f722b786bbe3", size = 484568, upload-time = "2024-09-04T20:44:23.245Z" }, + { url = "https://files.pythonhosted.org/packages/d4/38/ca8a4f639065f14ae0f1d9751e70447a261f1a30fa7547a828ae08142465/cffi-1.17.1-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:4ceb10419a9adf4460ea14cfd6bc43d08701f0835e979bf821052f1805850fe8", size = 488736, upload-time = "2024-09-04T20:44:24.757Z" }, + { url = "https://files.pythonhosted.org/packages/86/c5/28b2d6f799ec0bdecf44dced2ec5ed43e0eb63097b0f58c293583b406582/cffi-1.17.1-cp312-cp312-win32.whl", hash = "sha256:a08d7e755f8ed21095a310a693525137cfe756ce62d066e53f502a83dc550f65", size = 172448, upload-time = "2024-09-04T20:44:26.208Z" }, + { url = "https://files.pythonhosted.org/packages/50/b9/db34c4755a7bd1cb2d1603ac3863f22bcecbd1ba29e5ee841a4bc510b294/cffi-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:51392eae71afec0d0c8fb1a53b204dbb3bcabcb3c9b807eedf3e1e6ccf2de903", size = 181976, upload-time = "2024-09-04T20:44:27.578Z" }, + { 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" }, + { url = "https://files.pythonhosted.org/packages/0e/2d/eab2e858a91fdff70533cab61dcff4a1f55ec60425832ddfdc9cd36bc8af/cffi-1.17.1-cp313-cp313-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d01b12eeeb4427d3110de311e1774046ad344f5b1a7403101878976ecd7a10f3", size = 454792, upload-time = "2024-09-04T20:44:32.01Z" }, + { url = "https://files.pythonhosted.org/packages/75/b2/fbaec7c4455c604e29388d55599b99ebcc250a60050610fadde58932b7ee/cffi-1.17.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:706510fe141c86a69c8ddc029c7910003a17353970cff3b904ff0686a5927683", size = 478893, upload-time = "2024-09-04T20:44:33.606Z" }, + { url = "https://files.pythonhosted.org/packages/4f/b7/6e4a2162178bf1935c336d4da8a9352cccab4d3a5d7914065490f08c0690/cffi-1.17.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:de55b766c7aa2e2a3092c51e0483d700341182f08e67c63630d5b6f200bb28e5", size = 485810, upload-time = "2024-09-04T20:44:35.191Z" }, + { url = "https://files.pythonhosted.org/packages/c7/8a/1d0e4a9c26e54746dc08c2c6c037889124d4f59dffd853a659fa545f1b40/cffi-1.17.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c59d6e989d07460165cc5ad3c61f9fd8f1b4796eacbd81cee78957842b834af4", size = 471200, upload-time = "2024-09-04T20:44:36.743Z" }, + { url = "https://files.pythonhosted.org/packages/26/9f/1aab65a6c0db35f43c4d1b4f580e8df53914310afc10ae0397d29d697af4/cffi-1.17.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dd398dbc6773384a17fe0d3e7eeb8d1a21c2200473ee6806bb5e6a8e62bb73dd", size = 479447, upload-time = "2024-09-04T20:44:38.492Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e4/fb8b3dd8dc0e98edf1135ff067ae070bb32ef9d509d6cb0f538cd6f7483f/cffi-1.17.1-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:3edc8d958eb099c634dace3c7e16560ae474aa3803a5df240542b305d14e14ed", size = 484358, upload-time = "2024-09-04T20:44:40.046Z" }, + { url = "https://files.pythonhosted.org/packages/f1/47/d7145bf2dc04684935d57d67dff9d6d795b2ba2796806bb109864be3a151/cffi-1.17.1-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:72e72408cad3d5419375fc87d289076ee319835bdfa2caad331e377589aebba9", size = 488469, upload-time = "2024-09-04T20:44:41.616Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ee/f94057fa6426481d663b88637a9a10e859e492c73d0384514a17d78ee205/cffi-1.17.1-cp313-cp313-win32.whl", hash = "sha256:e03eab0a8677fa80d646b5ddece1cbeaf556c313dcfac435ba11f107ba117b5d", size = 172475, upload-time = "2024-09-04T20:44:43.733Z" }, + { url = "https://files.pythonhosted.org/packages/7c/fc/6a8cb64e5f0324877d503c854da15d76c1e50eb722e320b15345c4d0c6de/cffi-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:f6a16c31041f09ead72d69f583767292f750d24913dadacf5756b966aacb3f1a", size = 182009, upload-time = "2024-09-04T20:44:45.309Z" }, +] + +[[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/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.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/46/61/de6cd827efad202d7057d93e0fed9294b96952e188f7384832791c7b2254/click-8.3.0.tar.gz", hash = "sha256:e7b8232224eba16f4ebe410c25ced9f7875cb5f3263ffc93cc3e8da705e229c4", size = 276943, upload-time = "2025-09-18T17:32:23.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/d3/9dcc0f5797f070ec8edf30fbadfb200e71d9db6b84d211e3b2085a7589a0/click-8.3.0-py3-none-any.whl", hash = "sha256:9b9f285302c6e3064f4330c05f05b81945b2a39544279343e6e7c5f27a9baddc", size = 107295, upload-time = "2025-09-18T17:32:22.42Z" }, +] + +[[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 = "comm" +version = "0.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/a8/fb783cb0abe2b5fded9f55e5703015cdf1c9c85b3669087c538dd15a6a86/comm-0.2.2.tar.gz", hash = "sha256:3fd7a84065306e07bea1773df6eb8282de51ba82f77c72f9c85716ab11fe980e", size = 6210, upload-time = "2024-03-12T16:53:41.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/75/49e5bfe642f71f272236b5b2d2691cf915a7283cc0ceda56357b61daa538/comm-0.2.2-py3-none-any.whl", hash = "sha256:e6fb86cb70ff661ee8c9c14e7d36d6de3b4066f1441be4063df9c5009f0a64d3", size = 7180, upload-time = "2024-03-12T16:53:39.226Z" }, +] + +[[package]] +name = "cryptography" +version = "45.0.7" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/35/c495bffc2056f2dadb32434f1feedd79abde2a7f8363e1974afa9c33c7e2/cryptography-45.0.7.tar.gz", hash = "sha256:4b1654dfc64ea479c242508eb8c724044f1e964a47d1d1cacc5132292d851971", size = 744980, upload-time = "2025-09-01T11:15:03.146Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0c/91/925c0ac74362172ae4516000fe877912e33b5983df735ff290c653de4913/cryptography-45.0.7-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:3be4f21c6245930688bd9e162829480de027f8bf962ede33d4f8ba7d67a00cee", size = 7041105, upload-time = "2025-09-01T11:13:59.684Z" }, + { url = "https://files.pythonhosted.org/packages/fc/63/43641c5acce3a6105cf8bd5baeceeb1846bb63067d26dae3e5db59f1513a/cryptography-45.0.7-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:67285f8a611b0ebc0857ced2081e30302909f571a46bfa7a3cc0ad303fe015c6", size = 4205799, upload-time = "2025-09-01T11:14:02.517Z" }, + { url = "https://files.pythonhosted.org/packages/bc/29/c238dd9107f10bfde09a4d1c52fd38828b1aa353ced11f358b5dd2507d24/cryptography-45.0.7-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:577470e39e60a6cd7780793202e63536026d9b8641de011ed9d8174da9ca5339", size = 4430504, upload-time = "2025-09-01T11:14:04.522Z" }, + { url = "https://files.pythonhosted.org/packages/62/62/24203e7cbcc9bd7c94739428cd30680b18ae6b18377ae66075c8e4771b1b/cryptography-45.0.7-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:4bd3e5c4b9682bc112d634f2c6ccc6736ed3635fc3319ac2bb11d768cc5a00d8", size = 4209542, upload-time = "2025-09-01T11:14:06.309Z" }, + { url = "https://files.pythonhosted.org/packages/cd/e3/e7de4771a08620eef2389b86cd87a2c50326827dea5528feb70595439ce4/cryptography-45.0.7-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:465ccac9d70115cd4de7186e60cfe989de73f7bb23e8a7aa45af18f7412e75bf", size = 3889244, upload-time = "2025-09-01T11:14:08.152Z" }, + { url = "https://files.pythonhosted.org/packages/96/b8/bca71059e79a0bb2f8e4ec61d9c205fbe97876318566cde3b5092529faa9/cryptography-45.0.7-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:16ede8a4f7929b4b7ff3642eba2bf79aa1d71f24ab6ee443935c0d269b6bc513", size = 4461975, upload-time = "2025-09-01T11:14:09.755Z" }, + { url = "https://files.pythonhosted.org/packages/58/67/3f5b26937fe1218c40e95ef4ff8d23c8dc05aa950d54200cc7ea5fb58d28/cryptography-45.0.7-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:8978132287a9d3ad6b54fcd1e08548033cc09dc6aacacb6c004c73c3eb5d3ac3", size = 4209082, upload-time = "2025-09-01T11:14:11.229Z" }, + { url = "https://files.pythonhosted.org/packages/0e/e4/b3e68a4ac363406a56cf7b741eeb80d05284d8c60ee1a55cdc7587e2a553/cryptography-45.0.7-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:b6a0e535baec27b528cb07a119f321ac024592388c5681a5ced167ae98e9fff3", size = 4460397, upload-time = "2025-09-01T11:14:12.924Z" }, + { url = "https://files.pythonhosted.org/packages/22/49/2c93f3cd4e3efc8cb22b02678c1fad691cff9dd71bb889e030d100acbfe0/cryptography-45.0.7-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:a24ee598d10befaec178efdff6054bc4d7e883f615bfbcd08126a0f4931c83a6", size = 4337244, upload-time = "2025-09-01T11:14:14.431Z" }, + { url = "https://files.pythonhosted.org/packages/04/19/030f400de0bccccc09aa262706d90f2ec23d56bc4eb4f4e8268d0ddf3fb8/cryptography-45.0.7-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:fa26fa54c0a9384c27fcdc905a2fb7d60ac6e47d14bc2692145f2b3b1e2cfdbd", size = 4568862, upload-time = "2025-09-01T11:14:16.185Z" }, + { url = "https://files.pythonhosted.org/packages/29/56/3034a3a353efa65116fa20eb3c990a8c9f0d3db4085429040a7eef9ada5f/cryptography-45.0.7-cp311-abi3-win32.whl", hash = "sha256:bef32a5e327bd8e5af915d3416ffefdbe65ed975b646b3805be81b23580b57b8", size = 2936578, upload-time = "2025-09-01T11:14:17.638Z" }, + { url = "https://files.pythonhosted.org/packages/b3/61/0ab90f421c6194705a99d0fa9f6ee2045d916e4455fdbb095a9c2c9a520f/cryptography-45.0.7-cp311-abi3-win_amd64.whl", hash = "sha256:3808e6b2e5f0b46d981c24d79648e5c25c35e59902ea4391a0dcb3e667bf7443", size = 3405400, upload-time = "2025-09-01T11:14:18.958Z" }, + { url = "https://files.pythonhosted.org/packages/63/e8/c436233ddf19c5f15b25ace33979a9dd2e7aa1a59209a0ee8554179f1cc0/cryptography-45.0.7-cp37-abi3-macosx_10_9_universal2.whl", hash = "sha256:bfb4c801f65dd61cedfc61a83732327fafbac55a47282e6f26f073ca7a41c3b2", size = 7021824, upload-time = "2025-09-01T11:14:20.954Z" }, + { url = "https://files.pythonhosted.org/packages/bc/4c/8f57f2500d0ccd2675c5d0cc462095adf3faa8c52294ba085c036befb901/cryptography-45.0.7-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:81823935e2f8d476707e85a78a405953a03ef7b7b4f55f93f7c2d9680e5e0691", size = 4202233, upload-time = "2025-09-01T11:14:22.454Z" }, + { url = "https://files.pythonhosted.org/packages/eb/ac/59b7790b4ccaed739fc44775ce4645c9b8ce54cbec53edf16c74fd80cb2b/cryptography-45.0.7-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3994c809c17fc570c2af12c9b840d7cea85a9fd3e5c0e0491f4fa3c029216d59", size = 4423075, upload-time = "2025-09-01T11:14:24.287Z" }, + { url = "https://files.pythonhosted.org/packages/b8/56/d4f07ea21434bf891faa088a6ac15d6d98093a66e75e30ad08e88aa2b9ba/cryptography-45.0.7-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:dad43797959a74103cb59c5dac71409f9c27d34c8a05921341fb64ea8ccb1dd4", size = 4204517, upload-time = "2025-09-01T11:14:25.679Z" }, + { url = "https://files.pythonhosted.org/packages/e8/ac/924a723299848b4c741c1059752c7cfe09473b6fd77d2920398fc26bfb53/cryptography-45.0.7-cp37-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ce7a453385e4c4693985b4a4a3533e041558851eae061a58a5405363b098fcd3", size = 3882893, upload-time = "2025-09-01T11:14:27.1Z" }, + { url = "https://files.pythonhosted.org/packages/83/dc/4dab2ff0a871cc2d81d3ae6d780991c0192b259c35e4d83fe1de18b20c70/cryptography-45.0.7-cp37-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:b04f85ac3a90c227b6e5890acb0edbaf3140938dbecf07bff618bf3638578cf1", size = 4450132, upload-time = "2025-09-01T11:14:28.58Z" }, + { url = "https://files.pythonhosted.org/packages/12/dd/b2882b65db8fc944585d7fb00d67cf84a9cef4e77d9ba8f69082e911d0de/cryptography-45.0.7-cp37-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:48c41a44ef8b8c2e80ca4527ee81daa4c527df3ecbc9423c41a420a9559d0e27", size = 4204086, upload-time = "2025-09-01T11:14:30.572Z" }, + { url = "https://files.pythonhosted.org/packages/5d/fa/1d5745d878048699b8eb87c984d4ccc5da4f5008dfd3ad7a94040caca23a/cryptography-45.0.7-cp37-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:f3df7b3d0f91b88b2106031fd995802a2e9ae13e02c36c1fc075b43f420f3a17", size = 4449383, upload-time = "2025-09-01T11:14:32.046Z" }, + { url = "https://files.pythonhosted.org/packages/36/8b/fc61f87931bc030598e1876c45b936867bb72777eac693e905ab89832670/cryptography-45.0.7-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:dd342f085542f6eb894ca00ef70236ea46070c8a13824c6bde0dfdcd36065b9b", size = 4332186, upload-time = "2025-09-01T11:14:33.95Z" }, + { url = "https://files.pythonhosted.org/packages/0b/11/09700ddad7443ccb11d674efdbe9a832b4455dc1f16566d9bd3834922ce5/cryptography-45.0.7-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:1993a1bb7e4eccfb922b6cd414f072e08ff5816702a0bdb8941c247a6b1b287c", size = 4561639, upload-time = "2025-09-01T11:14:35.343Z" }, + { url = "https://files.pythonhosted.org/packages/71/ed/8f4c1337e9d3b94d8e50ae0b08ad0304a5709d483bfcadfcc77a23dbcb52/cryptography-45.0.7-cp37-abi3-win32.whl", hash = "sha256:18fcf70f243fe07252dcb1b268a687f2358025ce32f9f88028ca5c364b123ef5", size = 2926552, upload-time = "2025-09-01T11:14:36.929Z" }, + { url = "https://files.pythonhosted.org/packages/bc/ff/026513ecad58dacd45d1d24ebe52b852165a26e287177de1d545325c0c25/cryptography-45.0.7-cp37-abi3-win_amd64.whl", hash = "sha256:7285a89df4900ed3bfaad5679b1e668cb4b38a8de1ccbfc84b05f34512da0a90", size = 3392742, upload-time = "2025-09-01T11:14:38.368Z" }, +] + +[[package]] +name = "cryptography" +version = "46.0.0" +source = { registry = "https://pypi.org/simple" } +resolution-markers = [ + "python_full_version == '3.13.*' and platform_python_implementation != 'PyPy'", + "python_full_version >= '3.13' and platform_python_implementation == 'PyPy'", + "python_full_version < '3.13'", +] +dependencies = [ + { name = "cffi", marker = "python_full_version < '3.14' and platform_python_implementation != 'PyPy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/80/ee/04cd4314db26ffc951c1ea90bde30dd226880ab9343759d7abbecef377ee/cryptography-46.0.0.tar.gz", hash = "sha256:99f64a6d15f19f3afd78720ad2978f6d8d4c68cd4eb600fab82ab1a7c2071dca", size = 749158, upload-time = "2025-09-16T21:07:49.091Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/bd/3e935ca6e87dc4969683f5dd9e49adaf2cb5734253d93317b6b346e0bd33/cryptography-46.0.0-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:c9c4121f9a41cc3d02164541d986f59be31548ad355a5c96ac50703003c50fb7", size = 7285468, upload-time = "2025-09-16T21:05:52.026Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ee/dd17f412ce64b347871d7752657c5084940d42af4d9c25b1b91c7ee53362/cryptography-46.0.0-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:4f70cbade61a16f5e238c4b0eb4e258d177a2fcb59aa0aae1236594f7b0ae338", size = 4308218, upload-time = "2025-09-16T21:05:55.653Z" }, + { url = "https://files.pythonhosted.org/packages/2f/53/f0b865a971e4e8b3e90e648b6f828950dea4c221bb699421e82ef45f0ef9/cryptography-46.0.0-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1eccae15d5c28c74b2bea228775c63ac5b6c36eedb574e002440c0bc28750d3", size = 4571982, upload-time = "2025-09-16T21:05:57.322Z" }, + { url = "https://files.pythonhosted.org/packages/d4/c8/035be5fd63a98284fd74df9e04156f9fed7aa45cef41feceb0d06cbdadd0/cryptography-46.0.0-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:1b4fba84166d906a22027f0d958e42f3a4dbbb19c28ea71f0fb7812380b04e3c", size = 4307996, upload-time = "2025-09-16T21:05:59.043Z" }, + { url = "https://files.pythonhosted.org/packages/aa/4a/dbb6d7d0a48b95984e2d4caf0a4c7d6606cea5d30241d984c0c02b47f1b6/cryptography-46.0.0-cp311-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:523153480d7575a169933f083eb47b1edd5fef45d87b026737de74ffeb300f69", size = 4015692, upload-time = "2025-09-16T21:06:01.324Z" }, + { url = "https://files.pythonhosted.org/packages/65/48/aafcffdde716f6061864e56a0a5908f08dcb8523dab436228957c8ebd5df/cryptography-46.0.0-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:f09a3a108223e319168b7557810596631a8cb864657b0c16ed7a6017f0be9433", size = 4982192, upload-time = "2025-09-16T21:06:03.367Z" }, + { url = "https://files.pythonhosted.org/packages/4c/ab/1e73cfc181afc3054a09e5e8f7753a8fba254592ff50b735d7456d197353/cryptography-46.0.0-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:c1f6ccd6f2eef3b2eb52837f0463e853501e45a916b3fc42e5d93cf244a4b97b", size = 4603944, upload-time = "2025-09-16T21:06:05.29Z" }, + { url = "https://files.pythonhosted.org/packages/3a/02/d71dac90b77c606c90c366571edf264dc8bd37cf836e7f902253cbf5aa77/cryptography-46.0.0-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:80a548a5862d6912a45557a101092cd6c64ae1475b82cef50ee305d14a75f598", size = 4308149, upload-time = "2025-09-16T21:06:07.006Z" }, + { url = "https://files.pythonhosted.org/packages/29/e6/4dcb67fdc6addf4e319a99c4bed25776cb691f3aa6e0c4646474748816c6/cryptography-46.0.0-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:6c39fd5cd9b7526afa69d64b5e5645a06e1b904f342584b3885254400b63f1b3", size = 4947449, upload-time = "2025-09-16T21:06:11.244Z" }, + { url = "https://files.pythonhosted.org/packages/26/04/91e3fad8ee33aa87815c8f25563f176a58da676c2b14757a4d3b19f0253c/cryptography-46.0.0-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:d5c0cbb2fb522f7e39b59a5482a1c9c5923b7c506cfe96a1b8e7368c31617ac0", size = 4603549, upload-time = "2025-09-16T21:06:13.268Z" }, + { url = "https://files.pythonhosted.org/packages/9c/6e/caf4efadcc8f593cbaacfbb04778f78b6d0dac287b45cec25e5054de38b7/cryptography-46.0.0-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:6d8945bc120dcd90ae39aa841afddaeafc5f2e832809dc54fb906e3db829dfdc", size = 4435976, upload-time = "2025-09-16T21:06:16.514Z" }, + { url = "https://files.pythonhosted.org/packages/c1/c0/704710f349db25c5b91965c3662d5a758011b2511408d9451126429b6cd6/cryptography-46.0.0-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:88c09da8a94ac27798f6b62de6968ac78bb94805b5d272dbcfd5fdc8c566999f", size = 4709447, upload-time = "2025-09-16T21:06:19.246Z" }, + { url = "https://files.pythonhosted.org/packages/91/5e/ff63bfd27b75adaf75cc2398de28a0b08105f9d7f8193f3b9b071e38e8b9/cryptography-46.0.0-cp311-abi3-win32.whl", hash = "sha256:3738f50215211cee1974193a1809348d33893696ce119968932ea117bcbc9b1d", size = 3058317, upload-time = "2025-09-16T21:06:21.466Z" }, + { url = "https://files.pythonhosted.org/packages/46/47/4caf35014c4551dd0b43aa6c2e250161f7ffcb9c3918c9e075785047d5d2/cryptography-46.0.0-cp311-abi3-win_amd64.whl", hash = "sha256:bbaa5eef3c19c66613317dc61e211b48d5f550db009c45e1c28b59d5a9b7812a", size = 3523891, upload-time = "2025-09-16T21:06:23.856Z" }, + { url = "https://files.pythonhosted.org/packages/98/66/6a0cafb3084a854acf808fccf756cbc9b835d1b99fb82c4a15e2e2ffb404/cryptography-46.0.0-cp311-abi3-win_arm64.whl", hash = "sha256:16b5ac72a965ec9d1e34d9417dbce235d45fa04dac28634384e3ce40dfc66495", size = 2932145, upload-time = "2025-09-16T21:06:25.842Z" }, + { url = "https://files.pythonhosted.org/packages/f2/5f/0cf967a1dc1419d5dde111bd0e22872038199f4e4655539ea6f4da5ad7f1/cryptography-46.0.0-cp314-abi3-macosx_10_9_universal2.whl", hash = "sha256:91585fc9e696abd7b3e48a463a20dda1a5c0eeeca4ba60fa4205a79527694390", size = 7203952, upload-time = "2025-09-16T21:06:28.21Z" }, + { url = "https://files.pythonhosted.org/packages/9c/9e/d20925af5f0484c5049cf7254c91b79776a9b555af04493de6bdd419b495/cryptography-46.0.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:65e9117ebed5b16b28154ed36b164c20021f3a480e9cbb4b4a2a59b95e74c25d", size = 4293519, upload-time = "2025-09-16T21:06:30.143Z" }, + { url = "https://files.pythonhosted.org/packages/5f/b9/07aec6b183ef0054b5f826ae43f0b4db34c50b56aff18f67babdcc2642a3/cryptography-46.0.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:da7f93551d39d462263b6b5c9056c49f780b9200bf9fc2656d7c88c7bdb9b363", size = 4545583, upload-time = "2025-09-16T21:06:31.914Z" }, + { url = "https://files.pythonhosted.org/packages/39/4a/7d25158be8c607e2b9ebda49be762404d675b47df335d0d2a3b979d80213/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:be7479f9504bfb46628544ec7cb4637fe6af8b70445d4455fbb9c395ad9b7290", size = 4299196, upload-time = "2025-09-16T21:06:33.724Z" }, + { url = "https://files.pythonhosted.org/packages/15/3f/65c8753c0dbebe769cc9f9d87d52bce8b74e850ef2818c59bfc7e4248663/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f85e6a7d42ad60024fa1347b1d4ef82c4df517a4deb7f829d301f1a92ded038c", size = 3994419, upload-time = "2025-09-16T21:06:35.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b4/69a271873cfc333a236443c94aa07e0233bc36b384e182da2263703b5759/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:d349af4d76a93562f1dce4d983a4a34d01cb22b48635b0d2a0b8372cdb4a8136", size = 4960228, upload-time = "2025-09-16T21:06:38.182Z" }, + { url = "https://files.pythonhosted.org/packages/af/e0/ab62ee938b8d17bd1025cff569803cfc1c62dfdf89ffc78df6e092bff35f/cryptography-46.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:35aa1a44bd3e0efc3ef09cf924b3a0e2a57eda84074556f4506af2d294076685", size = 4577257, upload-time = "2025-09-16T21:06:39.998Z" }, + { url = "https://files.pythonhosted.org/packages/49/67/09a581c21da7189676678edd2bd37b64888c88c2d2727f2c3e0350194fba/cryptography-46.0.0-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:c457ad3f151d5fb380be99425b286167b358f76d97ad18b188b68097193ed95a", size = 4299023, upload-time = "2025-09-16T21:06:42.182Z" }, + { url = "https://files.pythonhosted.org/packages/af/28/2cb6d3d0d2c8ce8be4f19f4d83956c845c760a9e6dfe5b476cebed4f4f00/cryptography-46.0.0-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:399ef4c9be67f3902e5ca1d80e64b04498f8b56c19e1bc8d0825050ea5290410", size = 4925802, upload-time = "2025-09-16T21:06:44.31Z" }, + { url = "https://files.pythonhosted.org/packages/88/0b/1f31b6658c1dfa04e82b88de2d160e0e849ffb94353b1526dfb3a225a100/cryptography-46.0.0-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:378eff89b040cbce6169528f130ee75dceeb97eef396a801daec03b696434f06", size = 4577107, upload-time = "2025-09-16T21:06:46.324Z" }, + { url = "https://files.pythonhosted.org/packages/c2/af/507de3a1d4ded3068ddef188475d241bfc66563d99161585c8f2809fee01/cryptography-46.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c3648d6a5878fd1c9a22b1d43fa75efc069d5f54de12df95c638ae7ba88701d0", size = 4422506, upload-time = "2025-09-16T21:06:47.963Z" }, + { url = "https://files.pythonhosted.org/packages/47/aa/08e514756504d92334cabfe7fe792d10d977f2294ef126b2056b436450eb/cryptography-46.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:2fc30be952dd4334801d345d134c9ef0e9ccbaa8c3e1bc18925cbc4247b3e29c", size = 4684081, upload-time = "2025-09-16T21:06:49.667Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/ffde6e334fbd4ace04a6d9ced4c5fe1ca9e6ded4ee21b077a6889b452a89/cryptography-46.0.0-cp314-cp314t-win32.whl", hash = "sha256:b8e7db4ce0b7297e88f3d02e6ee9a39382e0efaf1e8974ad353120a2b5a57ef7", size = 3029735, upload-time = "2025-09-16T21:06:51.301Z" }, + { url = "https://files.pythonhosted.org/packages/4a/78/a41aee8bc5659390806196b0ed4d388211d3b38172827e610a82a7cd7546/cryptography-46.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:40ee4ce3c34acaa5bc347615ec452c74ae8ff7db973a98c97c62293120f668c6", size = 3502172, upload-time = "2025-09-16T21:06:53.328Z" }, + { url = "https://files.pythonhosted.org/packages/f0/2b/7e7427c258fdeae867d236cc9cad0c5c56735bc4d2f4adf035933ab4c15f/cryptography-46.0.0-cp314-cp314t-win_arm64.whl", hash = "sha256:07a1be54f995ce14740bf8bbe1cc35f7a37760f992f73cf9f98a2a60b9b97419", size = 2912344, upload-time = "2025-09-16T21:06:56.808Z" }, + { url = "https://files.pythonhosted.org/packages/53/06/80e7256a4677c2e9eb762638e8200a51f6dd56d2e3de3e34d0a83c2f5f80/cryptography-46.0.0-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:1d2073313324226fd846e6b5fc340ed02d43fd7478f584741bd6b791c33c9fee", size = 7257206, upload-time = "2025-09-16T21:06:59.295Z" }, + { url = "https://files.pythonhosted.org/packages/3d/b8/a5ed987f5c11b242713076121dddfff999d81fb492149c006a579d0e4099/cryptography-46.0.0-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83af84ebe7b6e9b6de05050c79f8cc0173c864ce747b53abce6a11e940efdc0d", size = 4301182, upload-time = "2025-09-16T21:07:01.624Z" }, + { url = "https://files.pythonhosted.org/packages/da/94/f1c1f30110c05fa5247bf460b17acfd52fa3f5c77e94ba19cff8957dc5e6/cryptography-46.0.0-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c3cd09b1490c1509bf3892bde9cef729795fae4a2fee0621f19be3321beca7e4", size = 4562561, upload-time = "2025-09-16T21:07:03.386Z" }, + { url = "https://files.pythonhosted.org/packages/5d/54/8decbf2f707350bedcd525833d3a0cc0203d8b080d926ad75d5c4de701ba/cryptography-46.0.0-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:d14eaf1569d6252280516bedaffdd65267428cdbc3a8c2d6de63753cf0863d5e", size = 4301974, upload-time = "2025-09-16T21:07:04.962Z" }, + { url = "https://files.pythonhosted.org/packages/82/63/c34a2f3516c6b05801f129616a5a1c68a8c403b91f23f9db783ee1d4f700/cryptography-46.0.0-cp38-abi3-manylinux_2_28_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:ab3a14cecc741c8c03ad0ad46dfbf18de25218551931a23bca2731d46c706d83", size = 4009462, upload-time = "2025-09-16T21:07:06.569Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c5/92ef920a4cf8ff35fcf9da5a09f008a6977dcb9801c709799ec1bf2873fb/cryptography-46.0.0-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:8e8b222eb54e3e7d3743a7c2b1f7fa7df7a9add790307bb34327c88ec85fe087", size = 4980769, upload-time = "2025-09-16T21:07:08.269Z" }, + { url = "https://files.pythonhosted.org/packages/a9/8f/1705f7ea3b9468c4a4fef6cce631db14feb6748499870a4772993cbeb729/cryptography-46.0.0-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:7f3f88df0c9b248dcc2e76124f9140621aca187ccc396b87bc363f890acf3a30", size = 4591812, upload-time = "2025-09-16T21:07:10.288Z" }, + { url = "https://files.pythonhosted.org/packages/34/b9/2d797ce9d346b8bac9f570b43e6e14226ff0f625f7f6f2f95d9065e316e3/cryptography-46.0.0-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:9aa85222f03fdb30defabc7a9e1e3d4ec76eb74ea9fe1504b2800844f9c98440", size = 4301844, upload-time = "2025-09-16T21:07:12.522Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2d/8efc9712997b46aea2ac8f74adc31f780ac4662e3b107ecad0d5c1a0c7f8/cryptography-46.0.0-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:f9aaf2a91302e1490c068d2f3af7df4137ac2b36600f5bd26e53d9ec320412d3", size = 4943257, upload-time = "2025-09-16T21:07:14.289Z" }, + { url = "https://files.pythonhosted.org/packages/c4/0c/bc365287a97d28aa7feef8810884831b2a38a8dc4cf0f8d6927ad1568d27/cryptography-46.0.0-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:32670ca085150ff36b438c17f2dfc54146fe4a074ebf0a76d72fb1b419a974bc", size = 4591154, upload-time = "2025-09-16T21:07:16.271Z" }, + { url = "https://files.pythonhosted.org/packages/51/3b/0b15107277b0c558c02027da615f4e78c892f22c6a04d29c6ad43fcddca6/cryptography-46.0.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0f58183453032727a65e6605240e7a3824fd1d6a7e75d2b537e280286ab79a52", size = 4428200, upload-time = "2025-09-16T21:07:18.118Z" }, + { url = "https://files.pythonhosted.org/packages/cf/24/814d69418247ea2cfc985eec6678239013500d745bc7a0a35a32c2e2f3be/cryptography-46.0.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:4bc257c2d5d865ed37d0bd7c500baa71f939a7952c424f28632298d80ccd5ec1", size = 4699862, upload-time = "2025-09-16T21:07:20.219Z" }, + { url = "https://files.pythonhosted.org/packages/fb/1e/665c718e0c45281a4e22454fa8a9bd8835f1ceb667b9ffe807baa41cd681/cryptography-46.0.0-cp38-abi3-win32.whl", hash = "sha256:df932ac70388be034b2e046e34d636245d5eeb8140db24a6b4c2268cd2073270", size = 3043766, upload-time = "2025-09-16T21:07:21.969Z" }, + { url = "https://files.pythonhosted.org/packages/78/7e/12e1e13abff381c702697845d1cf372939957735f49ef66f2061f38da32f/cryptography-46.0.0-cp38-abi3-win_amd64.whl", hash = "sha256:274f8b2eb3616709f437326185eb563eb4e5813d01ebe2029b61bfe7d9995fbb", size = 3517216, upload-time = "2025-09-16T21:07:24.024Z" }, + { url = "https://files.pythonhosted.org/packages/ad/55/009497b2ae7375db090b41f9fe7a1a7362f804ddfe17ed9e34f748fcb0e5/cryptography-46.0.0-cp38-abi3-win_arm64.whl", hash = "sha256:249c41f2bbfa026615e7bdca47e4a66135baa81b08509ab240a2e666f6af5966", size = 2923145, upload-time = "2025-09-16T21:07:25.74Z" }, +] + +[[package]] +name = "debugpy" +version = "1.8.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/bd/75/087fe07d40f490a78782ff3b0a30e3968936854105487decdb33446d4b0e/debugpy-1.8.14.tar.gz", hash = "sha256:7cd287184318416850aa8b60ac90105837bb1e59531898c07569d197d2ed5322", size = 1641444, upload-time = "2025-04-10T19:46:10.981Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/2a/ac2df0eda4898f29c46eb6713a5148e6f8b2b389c8ec9e425a4a1d67bf07/debugpy-1.8.14-cp312-cp312-macosx_14_0_universal2.whl", hash = "sha256:8899c17920d089cfa23e6005ad9f22582fd86f144b23acb9feeda59e84405b84", size = 2501268, upload-time = "2025-04-10T19:46:26.044Z" }, + { url = "https://files.pythonhosted.org/packages/10/53/0a0cb5d79dd9f7039169f8bf94a144ad3efa52cc519940b3b7dde23bcb89/debugpy-1.8.14-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f6bb5c0dcf80ad5dbc7b7d6eac484e2af34bdacdf81df09b6a3e62792b722826", size = 4221077, upload-time = "2025-04-10T19:46:27.464Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d5/84e01821f362327bf4828728aa31e907a2eca7c78cd7c6ec062780d249f8/debugpy-1.8.14-cp312-cp312-win32.whl", hash = "sha256:281d44d248a0e1791ad0eafdbbd2912ff0de9eec48022a5bfbc332957487ed3f", size = 5255127, upload-time = "2025-04-10T19:46:29.467Z" }, + { url = "https://files.pythonhosted.org/packages/33/16/1ed929d812c758295cac7f9cf3dab5c73439c83d9091f2d91871e648093e/debugpy-1.8.14-cp312-cp312-win_amd64.whl", hash = "sha256:5aa56ef8538893e4502a7d79047fe39b1dae08d9ae257074c6464a7b290b806f", size = 5297249, upload-time = "2025-04-10T19:46:31.538Z" }, + { url = "https://files.pythonhosted.org/packages/4d/e4/395c792b243f2367d84202dc33689aa3d910fb9826a7491ba20fc9e261f5/debugpy-1.8.14-cp313-cp313-macosx_14_0_universal2.whl", hash = "sha256:329a15d0660ee09fec6786acdb6e0443d595f64f5d096fc3e3ccf09a4259033f", size = 2485676, upload-time = "2025-04-10T19:46:32.96Z" }, + { url = "https://files.pythonhosted.org/packages/ba/f1/6f2ee3f991327ad9e4c2f8b82611a467052a0fb0e247390192580e89f7ff/debugpy-1.8.14-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f920c7f9af409d90f5fd26e313e119d908b0dd2952c2393cd3247a462331f15", size = 4217514, upload-time = "2025-04-10T19:46:34.336Z" }, + { url = "https://files.pythonhosted.org/packages/79/28/b9d146f8f2dc535c236ee09ad3e5ac899adb39d7a19b49f03ac95d216beb/debugpy-1.8.14-cp313-cp313-win32.whl", hash = "sha256:3784ec6e8600c66cbdd4ca2726c72d8ca781e94bce2f396cc606d458146f8f4e", size = 5254756, upload-time = "2025-04-10T19:46:36.199Z" }, + { url = "https://files.pythonhosted.org/packages/e0/62/a7b4a57013eac4ccaef6977966e6bec5c63906dd25a86e35f155952e29a1/debugpy-1.8.14-cp313-cp313-win_amd64.whl", hash = "sha256:684eaf43c95a3ec39a96f1f5195a7ff3d4144e4a18d69bb66beeb1a6de605d6e", size = 5297119, upload-time = "2025-04-10T19:46:38.141Z" }, + { url = "https://files.pythonhosted.org/packages/97/1a/481f33c37ee3ac8040d3d51fc4c4e4e7e61cb08b8bc8971d6032acc2279f/debugpy-1.8.14-py2.py3-none-any.whl", hash = "sha256:5cd9a579d553b6cb9759a7908a41988ee6280b961f24f63336835d9418216a20", size = 5256230, upload-time = "2025-04-10T19:46:54.077Z" }, +] + +[[package]] +name = "decorator" +version = "5.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/43/fa/6d96a0978d19e17b68d634497769987b16c8f4cd0a7a05048bec693caa6b/decorator-5.2.1.tar.gz", hash = "sha256:65f266143752f734b0a7cc83c46f4618af75b8c5911b00ccb61d0ac9b6da0360", size = 56711, upload-time = "2025-02-24T04:41:34.073Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/8c/f3147f5c4b73e7550fe5f9352eaa956ae838d5c51eb58e7a25b9f3e2643b/decorator-5.2.1-py3-none-any.whl", hash = "sha256:d316bb415a2d9e2d2b3abcc4084c6502fc09240e292cd76a76afc106a1c8e04a", size = 9190, upload-time = "2025-02-24T04:41:32.565Z" }, +] + +[[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 = "deprecation" +version = "2.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5a/d3/8ae2869247df154b64c1884d7346d412fed0c49df84db635aab2d1c40e62/deprecation-2.1.0.tar.gz", hash = "sha256:72b3bde64e5d778694b0cf68178aed03d15e15477116add3fb773e581f9518ff", size = 173788, upload-time = "2020-04-20T14:23:38.738Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/c3/253a89ee03fc9b9682f1541728eb66db7db22148cd94f89ab22528cd1e1b/deprecation-2.1.0-py2.py3-none-any.whl", hash = "sha256:a10811591210e1fb0e768a8c25517cabeabcba6f0bf96564f8ff45189f90b14a", size = 11178, upload-time = "2020-04-20T14:23:36.581Z" }, +] + +[[package]] +name = "dotenv" +version = "0.9.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "python-dotenv" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/b7/545d2c10c1fc15e48653c91efde329a790f2eecfbbf2bd16003b5db2bab0/dotenv-0.9.9-py2.py3-none-any.whl", hash = "sha256:29cf74a087b31dafdb5a446b6d7e11cbce8ed2741540e2339c69fbef92c94ce9", size = 1892, upload-time = "2025-02-19T22:15:01.647Z" }, +] + +[[package]] +name = "executing" +version = "2.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/50/a9d80c47ff289c611ff12e63f7c5d13942c65d68125160cefd768c73e6e4/executing-2.2.0.tar.gz", hash = "sha256:5d108c028108fe2551d1a7b2e8b713341e2cb4fc0aa7dcf966fa4327a5226755", size = 978693, upload-time = "2025-01-22T15:41:29.403Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/8f/c4d9bafc34ad7ad5d8dc16dd1347ee0e507a52c3adb6bfa8887e1c6a26ba/executing-2.2.0-py2.py3-none-any.whl", hash = "sha256:11387150cad388d62750327a53d3339fad4888b39a6fe233c3afbb54ecffd3aa", size = 26702, upload-time = "2025-01-22T15:41:25.929Z" }, +] + +[[package]] +name = "fastapi" +version = "0.117.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "starlette" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7e/7e/d9788300deaf416178f61fb3c2ceb16b7d0dc9f82a08fdb87a5e64ee3cc7/fastapi-0.117.1.tar.gz", hash = "sha256:fb2d42082d22b185f904ca0ecad2e195b851030bd6c5e4c032d1c981240c631a", size = 307155, upload-time = "2025-09-20T20:16:56.663Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6d/45/d9d3e8eeefbe93be1c50060a9d9a9f366dba66f288bb518a9566a23a8631/fastapi-0.117.1-py3-none-any.whl", hash = "sha256:33c51a0d21cab2b9722d4e56dbb9316f3687155be6b276191790d8da03507552", size = 95959, upload-time = "2025-09-20T20:16:53.661Z" }, +] + +[[package]] +name = "fastjsonschema" +version = "2.21.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8b/50/4b769ce1ac4071a1ef6d86b1a3fb56cdc3a37615e8c5519e1af96cdac366/fastjsonschema-2.21.1.tar.gz", hash = "sha256:794d4f0a58f848961ba16af7b9c85a3e88cd360df008c59aac6fc5ae9323b5d4", size = 373939, upload-time = "2024-12-02T10:55:15.133Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/2b/0817a2b257fe88725c25589d89aec060581aabf668707a8d03b2e9e0cb2a/fastjsonschema-2.21.1-py3-none-any.whl", hash = "sha256:c9e5b7e908310918cf494a434eeb31384dd84a98b57a30bcb1f535015b554667", size = 23924, upload-time = "2024-12-02T10:55:07.599Z" }, +] + +[[package]] +name = "ffmpy" +version = "0.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/f6/67cadf1686030be511004e75fa1c1397f8f193cd4d15d4788edef7c28621/ffmpy-0.6.1.tar.gz", hash = "sha256:b5830fd05f72bace05b8fb28724d54a7a63c5119d7f74ca36a75df33f749142d", size = 4958, upload-time = "2025-07-22T12:08:22.276Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/74/d4/1806897b31c480efc4e97c22506ac46c716084f573aef780bb7fb7a16e8a/ffmpy-0.6.1-py3-none-any.whl", hash = "sha256:69a37e2d7d6feb840e233d5640f3499a8b0a8657336774c86e4c52a3219222d4", size = 5512, upload-time = "2025-07-22T12:08:21.176Z" }, +] + +[[package]] +name = "filelock" +version = "3.18.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0a/10/c23352565a6544bdc5353e0b15fc1c563352101f30e24bf500207a54df9a/filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2", size = 18075, upload-time = "2025-03-14T07:11:40.47Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/36/2a115987e2d8c300a974597416d9de88f2444426de9571f4b59b2cca3acc/filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de", size = 16215, upload-time = "2025-03-14T07:11:39.145Z" }, +] + +[[package]] +name = "fqdn" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/30/3e/a80a8c077fd798951169626cde3e239adeba7dab75deb3555716415bd9b0/fqdn-1.5.1.tar.gz", hash = "sha256:105ed3677e767fb5ca086a0c1f4bb66ebc3c100be518f0e0d755d9eae164d89f", size = 6015, upload-time = "2021-03-11T07:16:29.08Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cf/58/8acf1b3e91c58313ce5cb67df61001fc9dcd21be4fadb76c1a2d540e09ed/fqdn-1.5.1-py3-none-any.whl", hash = "sha256:3a179af3761e4df6eb2e026ff9e1a3033d3587bf980a0b1b2e1e5d08d7358014", size = 9121, upload-time = "2021-03-11T07:16:28.351Z" }, +] + +[[package]] +name = "fsspec" +version = "2025.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/00/f7/27f15d41f0ed38e8fcc488584b57e902b331da7f7c6dcda53721b15838fc/fsspec-2025.5.1.tar.gz", hash = "sha256:2e55e47a540b91843b755e83ded97c6e897fa0942b11490113f09e9c443c2475", size = 303033, upload-time = "2025-05-24T12:03:23.792Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bb/61/78c7b3851add1481b048b5fdc29067397a1784e2910592bc81bb3f608635/fsspec-2025.5.1-py3-none-any.whl", hash = "sha256:24d3a2e663d5fc735ab256263c4075f374a174c3410c0b25e5bd1970bceaa462", size = 199052, upload-time = "2025-05-24T12:03:21.66Z" }, +] + +[[package]] +name = "gitdb" +version = "4.0.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "smmap" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, +] + +[[package]] +name = "gitpython" +version = "3.1.45" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "gitdb" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9a/c8/dd58967d119baab745caec2f9d853297cec1989ec1d63f677d3880632b88/gitpython-3.1.45.tar.gz", hash = "sha256:85b0ee964ceddf211c41b9f27a49086010a190fd8132a24e21f362a4b36a791c", size = 215076, upload-time = "2025-07-24T03:45:54.871Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/61/d4b89fec821f72385526e1b9d9a3a0385dda4a72b206d28049e2c7cd39b8/gitpython-3.1.45-py3-none-any.whl", hash = "sha256:8908cb2e02fb3b93b7eb0f2827125cb699869470432cc885f019b8fd0fccff77", size = 208168, upload-time = "2025-07-24T03:45:52.517Z" }, +] + +[[package]] +name = "gradio" +version = "5.47.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiofiles" }, + { name = "anyio" }, + { name = "audioop-lts", marker = "python_full_version >= '3.13'" }, + { name = "brotli" }, + { name = "fastapi" }, + { name = "ffmpy" }, + { name = "gradio-client" }, + { name = "groovy" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "jinja2" }, + { name = "markupsafe" }, + { name = "numpy" }, + { name = "orjson" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pillow" }, + { name = "pydantic" }, + { name = "pydub" }, + { name = "python-multipart" }, + { name = "pyyaml" }, + { name = "ruff" }, + { name = "safehttpx" }, + { name = "semantic-version" }, + { name = "starlette" }, + { name = "tomlkit" }, + { name = "typer" }, + { name = "typing-extensions" }, + { name = "uvicorn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/68/df/b792699b386c91aac38f5f844f92703a9fdd37aa4d2193c37de2cd4fa007/gradio-5.47.2.tar.gz", hash = "sha256:2e1cc00421da159ed9e9e2c8760e792ca2d8fa9bc610f3da0ec5cfa3fa6ca0be", size = 72289342, upload-time = "2025-09-26T19:51:10.355Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/44/7fed1186a9c289dad190011c1d86be761aeef968e856d653efa2f1d48dc9/gradio-5.47.2-py3-none-any.whl", hash = "sha256:e5cdf106b27bdb321284f327537682f3060ef0c62d9c70236eeaa8b1917a6803", size = 60369896, upload-time = "2025-09-26T19:51:05.636Z" }, +] + +[[package]] +name = "gradio-client" +version = "1.13.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fsspec" }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "packaging" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/a9/a3beb0ece8c05c33e6376b790fa42e0dd157abca8220cf639b249a597467/gradio_client-1.13.3.tar.gz", hash = "sha256:869b3e67e0f7a0f40df8c48c94de99183265cf4b7b1d9bd4623e336d219ffbe7", size = 323253, upload-time = "2025-09-26T19:51:21.7Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/0b/337b74504681b5dde39f20d803bb09757f9973ecdc65fd4e819d4b11faf7/gradio_client-1.13.3-py3-none-any.whl", hash = "sha256:3f63e4d33a2899c1a12b10fe3cf77b82a6919ff1a1fb6391f6aa225811aa390c", size = 325350, upload-time = "2025-09-26T19:51:20.288Z" }, +] + +[[package]] +name = "groovy" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/36/bbdede67400277bef33d3ec0e6a31750da972c469f75966b4930c753218f/groovy-0.1.2.tar.gz", hash = "sha256:25c1dc09b3f9d7e292458aa762c6beb96ea037071bf5e917fc81fb78d2231083", size = 17325, upload-time = "2025-02-28T20:24:56.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/27/3d6dcadc8a3214d8522c1e7f6a19554e33659be44546d44a2f7572ac7d2a/groovy-0.1.2-py3-none-any.whl", hash = "sha256:7f7975bab18c729a257a8b1ae9dcd70b7cafb1720481beae47719af57c35fa64", size = 14090, upload-time = "2025-02-28T20:24:55.152Z" }, +] + +[[package]] +name = "grpcio" +version = "1.74.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/38/b4/35feb8f7cab7239c5b94bd2db71abb3d6adb5f335ad8f131abb6060840b6/grpcio-1.74.0.tar.gz", hash = "sha256:80d1f4fbb35b0742d3e3d3bb654b7381cd5f015f8497279a1e9c21ba623e01b1", size = 12756048, upload-time = "2025-07-24T18:54:23.039Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4c/5d/e504d5d5c4469823504f65687d6c8fb97b7f7bf0b34873b7598f1df24630/grpcio-1.74.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:8533e6e9c5bd630ca98062e3a1326249e6ada07d05acf191a77bc33f8948f3d8", size = 5445551, upload-time = "2025-07-24T18:53:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/01/730e37056f96f2f6ce9f17999af1556df62ee8dab7fa48bceeaab5fd3008/grpcio-1.74.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:2918948864fec2a11721d91568effffbe0a02b23ecd57f281391d986847982f6", size = 10979810, upload-time = "2025-07-24T18:53:25.349Z" }, + { url = "https://files.pythonhosted.org/packages/79/3d/09fd100473ea5c47083889ca47ffd356576173ec134312f6aa0e13111dee/grpcio-1.74.0-cp312-cp312-manylinux_2_17_aarch64.whl", hash = "sha256:60d2d48b0580e70d2e1954d0d19fa3c2e60dd7cbed826aca104fff518310d1c5", size = 5941946, upload-time = "2025-07-24T18:53:27.387Z" }, + { url = "https://files.pythonhosted.org/packages/8a/99/12d2cca0a63c874c6d3d195629dcd85cdf5d6f98a30d8db44271f8a97b93/grpcio-1.74.0-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3601274bc0523f6dc07666c0e01682c94472402ac2fd1226fd96e079863bfa49", size = 6621763, upload-time = "2025-07-24T18:53:29.193Z" }, + { url = "https://files.pythonhosted.org/packages/9d/2c/930b0e7a2f1029bbc193443c7bc4dc2a46fedb0203c8793dcd97081f1520/grpcio-1.74.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:176d60a5168d7948539def20b2a3adcce67d72454d9ae05969a2e73f3a0feee7", size = 6180664, upload-time = "2025-07-24T18:53:30.823Z" }, + { url = "https://files.pythonhosted.org/packages/db/d5/ff8a2442180ad0867717e670f5ec42bfd8d38b92158ad6bcd864e6d4b1ed/grpcio-1.74.0-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:e759f9e8bc908aaae0412642afe5416c9f983a80499448fcc7fab8692ae044c3", size = 6301083, upload-time = "2025-07-24T18:53:32.454Z" }, + { url = "https://files.pythonhosted.org/packages/b0/ba/b361d390451a37ca118e4ec7dccec690422e05bc85fba2ec72b06cefec9f/grpcio-1.74.0-cp312-cp312-musllinux_1_1_i686.whl", hash = "sha256:9e7c4389771855a92934b2846bd807fc25a3dfa820fd912fe6bd8136026b2707", size = 6994132, upload-time = "2025-07-24T18:53:34.506Z" }, + { url = "https://files.pythonhosted.org/packages/3b/0c/3a5fa47d2437a44ced74141795ac0251bbddeae74bf81df3447edd767d27/grpcio-1.74.0-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:cce634b10aeab37010449124814b05a62fb5f18928ca878f1bf4750d1f0c815b", size = 6489616, upload-time = "2025-07-24T18:53:36.217Z" }, + { url = "https://files.pythonhosted.org/packages/ae/95/ab64703b436d99dc5217228babc76047d60e9ad14df129e307b5fec81fd0/grpcio-1.74.0-cp312-cp312-win32.whl", hash = "sha256:885912559974df35d92219e2dc98f51a16a48395f37b92865ad45186f294096c", size = 3807083, upload-time = "2025-07-24T18:53:37.911Z" }, + { url = "https://files.pythonhosted.org/packages/84/59/900aa2445891fc47a33f7d2f76e00ca5d6ae6584b20d19af9c06fa09bf9a/grpcio-1.74.0-cp312-cp312-win_amd64.whl", hash = "sha256:42f8fee287427b94be63d916c90399ed310ed10aadbf9e2e5538b3e497d269bc", size = 4490123, upload-time = "2025-07-24T18:53:39.528Z" }, + { url = "https://files.pythonhosted.org/packages/d4/d8/1004a5f468715221450e66b051c839c2ce9a985aa3ee427422061fcbb6aa/grpcio-1.74.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:2bc2d7d8d184e2362b53905cb1708c84cb16354771c04b490485fa07ce3a1d89", size = 5449488, upload-time = "2025-07-24T18:53:41.174Z" }, + { url = "https://files.pythonhosted.org/packages/94/0e/33731a03f63740d7743dced423846c831d8e6da808fcd02821a4416df7fa/grpcio-1.74.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:c14e803037e572c177ba54a3e090d6eb12efd795d49327c5ee2b3bddb836bf01", size = 10974059, upload-time = "2025-07-24T18:53:43.066Z" }, + { url = "https://files.pythonhosted.org/packages/0d/c6/3d2c14d87771a421205bdca991467cfe473ee4c6a1231c1ede5248c62ab8/grpcio-1.74.0-cp313-cp313-manylinux_2_17_aarch64.whl", hash = "sha256:f6ec94f0e50eb8fa1744a731088b966427575e40c2944a980049798b127a687e", size = 5945647, upload-time = "2025-07-24T18:53:45.269Z" }, + { url = "https://files.pythonhosted.org/packages/c5/83/5a354c8aaff58594eef7fffebae41a0f8995a6258bbc6809b800c33d4c13/grpcio-1.74.0-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:566b9395b90cc3d0d0c6404bc8572c7c18786ede549cdb540ae27b58afe0fb91", size = 6626101, upload-time = "2025-07-24T18:53:47.015Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ca/4fdc7bf59bf6994aa45cbd4ef1055cd65e2884de6113dbd49f75498ddb08/grpcio-1.74.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1ea6176d7dfd5b941ea01c2ec34de9531ba494d541fe2057c904e601879f249", size = 6182562, upload-time = "2025-07-24T18:53:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/fd/48/2869e5b2c1922583686f7ae674937986807c2f676d08be70d0a541316270/grpcio-1.74.0-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:64229c1e9cea079420527fa8ac45d80fc1e8d3f94deaa35643c381fa8d98f362", size = 6303425, upload-time = "2025-07-24T18:53:50.847Z" }, + { url = "https://files.pythonhosted.org/packages/a6/0e/bac93147b9a164f759497bc6913e74af1cb632c733c7af62c0336782bd38/grpcio-1.74.0-cp313-cp313-musllinux_1_1_i686.whl", hash = "sha256:0f87bddd6e27fc776aacf7ebfec367b6d49cad0455123951e4488ea99d9b9b8f", size = 6996533, upload-time = "2025-07-24T18:53:52.747Z" }, + { url = "https://files.pythonhosted.org/packages/84/35/9f6b2503c1fd86d068b46818bbd7329db26a87cdd8c01e0d1a9abea1104c/grpcio-1.74.0-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:3b03d8f2a07f0fea8c8f74deb59f8352b770e3900d143b3d1475effcb08eec20", size = 6491489, upload-time = "2025-07-24T18:53:55.06Z" }, + { url = "https://files.pythonhosted.org/packages/75/33/a04e99be2a82c4cbc4039eb3a76f6c3632932b9d5d295221389d10ac9ca7/grpcio-1.74.0-cp313-cp313-win32.whl", hash = "sha256:b6a73b2ba83e663b2480a90b82fdae6a7aa6427f62bf43b29912c0cfd1aa2bfa", size = 3805811, upload-time = "2025-07-24T18:53:56.798Z" }, + { url = "https://files.pythonhosted.org/packages/34/80/de3eb55eb581815342d097214bed4c59e806b05f1b3110df03b2280d6dfd/grpcio-1.74.0-cp313-cp313-win_amd64.whl", hash = "sha256:fd3c71aeee838299c5887230b8a1822795325ddfea635edd82954c1eaa831e24", size = 4489214, upload-time = "2025-07-24T18:53:59.771Z" }, +] + +[[package]] +name = "h11" +version = "0.16.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, +] + +[[package]] +name = "h2" +version = "4.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "hpack" }, + { name = "hyperframe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026, upload-time = "2025-08-23T18:12:19.778Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779, upload-time = "2025-08-23T18:12:17.779Z" }, +] + +[[package]] +name = "hf-xet" +version = "1.1.10" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/74/31/feeddfce1748c4a233ec1aa5b7396161c07ae1aa9b7bdbc9a72c3c7dd768/hf_xet-1.1.10.tar.gz", hash = "sha256:408aef343800a2102374a883f283ff29068055c111f003ff840733d3b715bb97", size = 487910, upload-time = "2025-09-12T20:10:27.12Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f7/a2/343e6d05de96908366bdc0081f2d8607d61200be2ac802769c4284cc65bd/hf_xet-1.1.10-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:686083aca1a6669bc85c21c0563551cbcdaa5cf7876a91f3d074a030b577231d", size = 2761466, upload-time = "2025-09-12T20:10:22.836Z" }, + { url = "https://files.pythonhosted.org/packages/31/f9/6215f948ac8f17566ee27af6430ea72045e0418ce757260248b483f4183b/hf_xet-1.1.10-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:71081925383b66b24eedff3013f8e6bbd41215c3338be4b94ba75fd75b21513b", size = 2623807, upload-time = "2025-09-12T20:10:21.118Z" }, + { url = "https://files.pythonhosted.org/packages/15/07/86397573efefff941e100367bbda0b21496ffcdb34db7ab51912994c32a2/hf_xet-1.1.10-cp37-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b6bceb6361c80c1cc42b5a7b4e3efd90e64630bcf11224dcac50ef30a47e435", size = 3186960, upload-time = "2025-09-12T20:10:19.336Z" }, + { url = "https://files.pythonhosted.org/packages/01/a7/0b2e242b918cc30e1f91980f3c4b026ff2eedaf1e2ad96933bca164b2869/hf_xet-1.1.10-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:eae7c1fc8a664e54753ffc235e11427ca61f4b0477d757cc4eb9ae374b69f09c", size = 3087167, upload-time = "2025-09-12T20:10:17.255Z" }, + { url = "https://files.pythonhosted.org/packages/4a/25/3e32ab61cc7145b11eee9d745988e2f0f4fafda81b25980eebf97d8cff15/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:0a0005fd08f002180f7a12d4e13b22be277725bc23ed0529f8add5c7a6309c06", size = 3248612, upload-time = "2025-09-12T20:10:24.093Z" }, + { url = "https://files.pythonhosted.org/packages/2c/3d/ab7109e607ed321afaa690f557a9ada6d6d164ec852fd6bf9979665dc3d6/hf_xet-1.1.10-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:f900481cf6e362a6c549c61ff77468bd59d6dd082f3170a36acfef2eb6a6793f", size = 3353360, upload-time = "2025-09-12T20:10:25.563Z" }, + { url = "https://files.pythonhosted.org/packages/ee/0e/471f0a21db36e71a2f1752767ad77e92d8cde24e974e03d662931b1305ec/hf_xet-1.1.10-cp37-abi3-win_amd64.whl", hash = "sha256:5f54b19cc347c13235ae7ee98b330c26dd65ef1df47e5316ffb1e87713ca7045", size = 2804691, upload-time = "2025-09-12T20:10:28.433Z" }, +] + +[[package]] +name = "hpack" +version = "4.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, +] + +[[package]] +name = "httpcore" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" }, +] + +[[package]] +name = "httpx" +version = "0.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "certifi" }, + { name = "httpcore" }, + { name = "idna" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, +] + +[package.optional-dependencies] +http2 = [ + { name = "h2" }, +] + +[[package]] +name = "huggingface-hub" +version = "0.35.1" +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/f6/42/0e7be334a6851cd7d51cc11717cb95e89333ebf0064431c0255c56957526/huggingface_hub-0.35.1.tar.gz", hash = "sha256:3585b88c5169c64b7e4214d0e88163d4a709de6d1a502e0cd0459e9ee2c9c572", size = 461374, upload-time = "2025-09-23T13:43:47.074Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/60/4acf0c8a3925d9ff491dc08fe84d37e09cfca9c3b885e0db3d4dedb98cea/huggingface_hub-0.35.1-py3-none-any.whl", hash = "sha256:2f0e2709c711e3040e31d3e0418341f7092910f1462dd00350c4e97af47280a8", size = 563340, upload-time = "2025-09-23T13:43:45.343Z" }, +] + +[[package]] +name = "hyperframe" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, +] + +[[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 = "ipykernel" +version = "6.29.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "appnope", marker = "sys_platform == 'darwin'" }, + { name = "comm" }, + { name = "debugpy" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "matplotlib-inline" }, + { name = "nest-asyncio" }, + { name = "packaging" }, + { name = "psutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e9/5c/67594cb0c7055dc50814b21731c22a601101ea3b1b50a9a1b090e11f5d0f/ipykernel-6.29.5.tar.gz", hash = "sha256:f093a22c4a40f8828f8e330a9c297cb93dcab13bd9678ded6de8e5cf81c56215", size = 163367, upload-time = "2024-07-01T14:07:22.543Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/5c/368ae6c01c7628438358e6d337c19b05425727fbb221d2a3c4303c372f42/ipykernel-6.29.5-py3-none-any.whl", hash = "sha256:afdb66ba5aa354b09b91379bac28ae4afebbb30e8b39510c9690afb7a10421b5", size = 117173, upload-time = "2024-07-01T14:07:19.603Z" }, +] + +[[package]] +name = "ipython" +version = "9.3.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "decorator" }, + { name = "ipython-pygments-lexers" }, + { name = "jedi" }, + { name = "matplotlib-inline" }, + { name = "pexpect", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "stack-data" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/09/4c7e06b96fbd203e06567b60fb41b06db606b6a82db6db7b2c85bb72a15c/ipython-9.3.0.tar.gz", hash = "sha256:79eb896f9f23f50ad16c3bc205f686f6e030ad246cc309c6279a242b14afe9d8", size = 4426460, upload-time = "2025-05-31T16:34:55.678Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/99/9ed3d52d00f1846679e3aa12e2326ac7044b5e7f90dc822b60115fa533ca/ipython-9.3.0-py3-none-any.whl", hash = "sha256:1a0b6dd9221a1f5dddf725b57ac0cb6fddc7b5f470576231ae9162b9b3455a04", size = 605320, upload-time = "2025-05-31T16:34:52.154Z" }, +] + +[[package]] +name = "ipython-pygments-lexers" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ef/4c/5dd1d8af08107f88c7f741ead7a40854b8ac24ddf9ae850afbcf698aa552/ipython_pygments_lexers-1.1.1.tar.gz", hash = "sha256:09c0138009e56b6854f9535736f4171d855c8c08a563a0dcd8022f78355c7e81", size = 8393, upload-time = "2025-01-17T11:24:34.505Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d9/33/1f075bf72b0b747cb3288d011319aaf64083cf2efef8354174e3ed4540e2/ipython_pygments_lexers-1.1.1-py3-none-any.whl", hash = "sha256:a9462224a505ade19a605f71f8fa63c2048833ce50abc86768a0d81d876dc81c", size = 8074, upload-time = "2025-01-17T11:24:33.271Z" }, +] + +[[package]] +name = "ipywidgets" +version = "8.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "comm" }, + { name = "ipython" }, + { name = "jupyterlab-widgets" }, + { name = "traitlets" }, + { name = "widgetsnbextension" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3e/48/d3dbac45c2814cb73812f98dd6b38bbcc957a4e7bb31d6ea9c03bf94ed87/ipywidgets-8.1.7.tar.gz", hash = "sha256:15f1ac050b9ccbefd45dccfbb2ef6bed0029d8278682d569d71b8dd96bee0376", size = 116721, upload-time = "2025-05-05T12:42:03.489Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/58/6a/9166369a2f092bd286d24e6307de555d63616e8ddb373ebad2b5635ca4cd/ipywidgets-8.1.7-py3-none-any.whl", hash = "sha256:764f2602d25471c213919b8a1997df04bef869251db4ca8efba1b76b1bd9f7bb", size = 139806, upload-time = "2025-05-05T12:41:56.833Z" }, +] + +[[package]] +name = "isoduration" +version = "20.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "arrow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7c/1a/3c8edc664e06e6bd06cce40c6b22da5f1429aa4224d0c590f3be21c91ead/isoduration-20.11.0.tar.gz", hash = "sha256:ac2f9015137935279eac671f94f89eb00584f940f5dc49462a0c4ee692ba1bd9", size = 11649, upload-time = "2020-11-01T11:00:00.312Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/55/e5326141505c5d5e34c5e0935d2908a74e4561eca44108fbfb9c13d2911a/isoduration-20.11.0-py3-none-any.whl", hash = "sha256:b2904c2a4228c3d44f409c8ae8e2370eb21a26f7ac2ec5446df141dde3452042", size = 11321, upload-time = "2020-11-01T10:59:58.02Z" }, +] + +[[package]] +name = "jedi" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "parso" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/72/3a/79a912fbd4d8dd6fbb02bf69afd3bb72cf0c729bb3063c6f4498603db17a/jedi-0.19.2.tar.gz", hash = "sha256:4770dc3de41bde3966b02eb84fbcf557fb33cce26ad23da12c742fb50ecb11f0", size = 1231287, upload-time = "2024-11-11T01:41:42.873Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/5a/9cac0c82afec3d09ccd97c8b6502d48f165f9124db81b4bcb90b4af974ee/jedi-0.19.2-py2.py3-none-any.whl", hash = "sha256:a8ef22bde8490f57fe5c7681a3c83cb58874daf72b4784de3cce5b6ef6edb5b9", size = 1572278, upload-time = "2024-11-11T01:41:40.175Z" }, +] + +[[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 = "joblib" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e8/5d/447af5ea094b9e4c4054f82e223ada074c552335b9b4b2d14bd9b35a67c4/joblib-1.5.2.tar.gz", hash = "sha256:3faa5c39054b2f03ca547da9b2f52fde67c06240c31853f306aea97f13647b55", size = 331077, upload-time = "2025-08-27T12:15:46.575Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/e8/685f47e0d754320684db4425a0967f7d3fa70126bffd76110b7009a0090f/joblib-1.5.2-py3-none-any.whl", hash = "sha256:4e1f0bdbb987e6d843c70cf43714cb276623def372df3c22fe5266b2670bc241", size = 308396, upload-time = "2025-08-27T12:15:45.188Z" }, +] + +[[package]] +name = "json5" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/12/be/c6c745ec4c4539b25a278b70e29793f10382947df0d9efba2fa09120895d/json5-0.12.0.tar.gz", hash = "sha256:0b4b6ff56801a1c7dc817b0241bca4ce474a0e6a163bfef3fc594d3fd263ff3a", size = 51907, upload-time = "2025-04-03T16:33:13.201Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/9f/3500910d5a98549e3098807493851eeef2b89cdd3032227558a104dfe926/json5-0.12.0-py3-none-any.whl", hash = "sha256:6d37aa6c08b0609f16e1ec5ff94697e2cbbfbad5ac112afa05794da9ab7810db", size = 36079, upload-time = "2025-04-03T16:33:11.927Z" }, +] + +[[package]] +name = "jsonpointer" +version = "3.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/0a/eebeb1fa92507ea94016a2a790b93c2ae41a7e18778f85471dc54475ed25/jsonpointer-3.0.0.tar.gz", hash = "sha256:2b2d729f2091522d61c3b31f82e11870f60b68f43fbc705cb76bf4b832af59ef", size = 9114, upload-time = "2024-06-10T19:24:42.462Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/92/5e77f98553e9e75130c78900d000368476aed74276eb8ae8796f65f00918/jsonpointer-3.0.0-py2.py3-none-any.whl", hash = "sha256:13e088adc14fca8b6aa8177c044e12701e6ad4b28ff10e65f2267a90109c9942", size = 7595, upload-time = "2024-06-10T19:24:40.698Z" }, +] + +[[package]] +name = "jsonschema" +version = "4.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "jsonschema-specifications" }, + { name = "referencing" }, + { name = "rpds-py" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/d3/1cf5326b923a53515d8f3a2cd442e6d7e94fcc444716e879ea70a0ce3177/jsonschema-4.24.0.tar.gz", hash = "sha256:0b4e8069eb12aedfa881333004bccaec24ecef5a8a6a4b6df142b2cc9599d196", size = 353480, upload-time = "2025-05-26T18:48:10.459Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/3d/023389198f69c722d039351050738d6755376c8fd343e91dc493ea485905/jsonschema-4.24.0-py3-none-any.whl", hash = "sha256:a462455f19f5faf404a7902952b6f0e3ce868f3ee09a359b05eca6673bd8412d", size = 88709, upload-time = "2025-05-26T18:48:08.417Z" }, +] + +[package.optional-dependencies] +format-nongpl = [ + { name = "fqdn" }, + { name = "idna" }, + { name = "isoduration" }, + { name = "jsonpointer" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "uri-template" }, + { name = "webcolors" }, +] + +[[package]] +name = "jsonschema-specifications" +version = "2025.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "referencing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bf/ce/46fbd9c8119cfc3581ee5643ea49464d168028cfb5caff5fc0596d0cf914/jsonschema_specifications-2025.4.1.tar.gz", hash = "sha256:630159c9f4dbea161a6a2205c3011cc4f18ff381b189fff48bb39b9bf26ae608", size = 15513, upload-time = "2025-04-23T12:34:07.418Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/0e/b27cdbaccf30b890c40ed1da9fd4a3593a5cf94dae54fb34f8a4b74fcd3f/jsonschema_specifications-2025.4.1-py3-none-any.whl", hash = "sha256:4653bffbd6584f7de83a67e0d620ef16900b390ddc7939d56684d6c81e33f1af", size = 18437, upload-time = "2025-04-23T12:34:05.422Z" }, +] + +[[package]] +name = "jupyter" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipywidgets" }, + { name = "jupyter-console" }, + { name = "jupyterlab" }, + { name = "nbconvert" }, + { name = "notebook" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/f3/af28ea964ab8bc1e472dba2e82627d36d470c51f5cd38c37502eeffaa25e/jupyter-1.1.1.tar.gz", hash = "sha256:d55467bceabdea49d7e3624af7e33d59c37fff53ed3a350e1ac957bed731de7a", size = 5714959, upload-time = "2024-08-30T07:15:48.299Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/38/64/285f20a31679bf547b75602702f7800e74dbabae36ef324f716c02804753/jupyter-1.1.1-py2.py3-none-any.whl", hash = "sha256:7a59533c22af65439b24bbe60373a4e95af8f16ac65a6c00820ad378e3f7cc83", size = 2657, upload-time = "2024-08-30T07:15:47.045Z" }, +] + +[[package]] +name = "jupyter-client" +version = "8.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-core" }, + { name = "python-dateutil" }, + { name = "pyzmq" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" }, +] + +[[package]] +name = "jupyter-console" +version = "6.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ipykernel" }, + { name = "ipython" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "prompt-toolkit" }, + { name = "pygments" }, + { name = "pyzmq" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/2d/e2fd31e2fc41c14e2bcb6c976ab732597e907523f6b2420305f9fc7fdbdb/jupyter_console-6.6.3.tar.gz", hash = "sha256:566a4bf31c87adbfadf22cdf846e3069b59a71ed5da71d6ba4d8aaad14a53539", size = 34363, upload-time = "2023-03-06T14:13:31.02Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/77/71d78d58f15c22db16328a476426f7ac4a60d3a5a7ba3b9627ee2f7903d4/jupyter_console-6.6.3-py3-none-any.whl", hash = "sha256:309d33409fcc92ffdad25f0bcdf9a4a9daa61b6f341177570fdac03de5352485", size = 24510, upload-time = "2023-03-06T14:13:28.229Z" }, +] + +[[package]] +name = "jupyter-core" +version = "5.8.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "platformdirs" }, + { name = "pywin32", marker = "platform_python_implementation != 'PyPy' and sys_platform == 'win32'" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/1b/72906d554acfeb588332eaaa6f61577705e9ec752ddb486f302dafa292d9/jupyter_core-5.8.1.tar.gz", hash = "sha256:0a5f9706f70e64786b75acba995988915ebd4601c8a52e534a40b51c95f59941", size = 88923, upload-time = "2025-05-27T07:38:16.655Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/57/6bffd4b20b88da3800c5d691e0337761576ee688eb01299eae865689d2df/jupyter_core-5.8.1-py3-none-any.whl", hash = "sha256:c28d268fc90fb53f1338ded2eb410704c5449a358406e8a948b75706e24863d0", size = 28880, upload-time = "2025-05-27T07:38:15.137Z" }, +] + +[[package]] +name = "jupyter-events" +version = "0.12.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jsonschema", extra = ["format-nongpl"] }, + { name = "packaging" }, + { name = "python-json-logger" }, + { name = "pyyaml" }, + { name = "referencing" }, + { name = "rfc3339-validator" }, + { name = "rfc3986-validator" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/c3/306d090461e4cf3cd91eceaff84bede12a8e52cd821c2d20c9a4fd728385/jupyter_events-0.12.0.tar.gz", hash = "sha256:fc3fce98865f6784c9cd0a56a20644fc6098f21c8c33834a8d9fe383c17e554b", size = 62196, upload-time = "2025-02-03T17:23:41.485Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e2/48/577993f1f99c552f18a0428731a755e06171f9902fa118c379eb7c04ea22/jupyter_events-0.12.0-py3-none-any.whl", hash = "sha256:6464b2fa5ad10451c3d35fabc75eab39556ae1e2853ad0c0cc31b656731a97fb", size = 19430, upload-time = "2025-02-03T17:23:38.643Z" }, +] + +[[package]] +name = "jupyter-lsp" +version = "2.2.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/b4/3200b0b09c12bc3b72d943d923323c398eff382d1dcc7c0dbc8b74630e40/jupyter-lsp-2.2.5.tar.gz", hash = "sha256:793147a05ad446f809fd53ef1cd19a9f5256fd0a2d6b7ce943a982cb4f545001", size = 48741, upload-time = "2024-04-09T17:59:44.918Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/e0/7bd7cff65594fd9936e2f9385701e44574fc7d721331ff676ce440b14100/jupyter_lsp-2.2.5-py3-none-any.whl", hash = "sha256:45fbddbd505f3fbfb0b6cb2f1bc5e15e83ab7c79cd6e89416b248cb3c00c11da", size = 69146, upload-time = "2024-04-09T17:59:43.388Z" }, +] + +[[package]] +name = "jupyter-server" +version = "2.16.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "argon2-cffi" }, + { name = "jinja2" }, + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "jupyter-events" }, + { name = "jupyter-server-terminals" }, + { name = "nbconvert" }, + { name = "nbformat" }, + { name = "overrides" }, + { name = "packaging" }, + { name = "prometheus-client" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "pyzmq" }, + { name = "send2trash" }, + { name = "terminado" }, + { name = "tornado" }, + { name = "traitlets" }, + { name = "websocket-client" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/41/c8/ba2bbcd758c47f1124c4ca14061e8ce60d9c6fd537faee9534a95f83521a/jupyter_server-2.16.0.tar.gz", hash = "sha256:65d4b44fdf2dcbbdfe0aa1ace4a842d4aaf746a2b7b168134d5aaed35621b7f6", size = 728177, upload-time = "2025-05-12T16:44:46.245Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/1f/5ebbced977171d09a7b0c08a285ff9a20aafb9c51bde07e52349ff1ddd71/jupyter_server-2.16.0-py3-none-any.whl", hash = "sha256:3d8db5be3bc64403b1c65b400a1d7f4647a5ce743f3b20dbdefe8ddb7b55af9e", size = 386904, upload-time = "2025-05-12T16:44:43.335Z" }, +] + +[[package]] +name = "jupyter-server-terminals" +version = "0.5.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "terminado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fc/d5/562469734f476159e99a55426d697cbf8e7eb5efe89fb0e0b4f83a3d3459/jupyter_server_terminals-0.5.3.tar.gz", hash = "sha256:5ae0295167220e9ace0edcfdb212afd2b01ee8d179fe6f23c899590e9b8a5269", size = 31430, upload-time = "2024-03-12T14:37:03.049Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/07/2d/2b32cdbe8d2a602f697a649798554e4f072115438e92249624e532e8aca6/jupyter_server_terminals-0.5.3-py3-none-any.whl", hash = "sha256:41ee0d7dc0ebf2809c668e0fc726dfaf258fcd3e769568996ca731b6194ae9aa", size = 13656, upload-time = "2024-03-12T14:37:00.708Z" }, +] + +[[package]] +name = "jupyterlab" +version = "4.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "async-lru" }, + { name = "httpx" }, + { name = "ipykernel" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyter-lsp" }, + { name = "jupyter-server" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "packaging" }, + { name = "setuptools" }, + { name = "tornado" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d3/2d/d1678dcf2db66cb4a38a80d9e5fcf48c349f3ac12f2d38882993353ae768/jupyterlab-4.4.3.tar.gz", hash = "sha256:a94c32fd7f8b93e82a49dc70a6ec45a5c18281ca2a7228d12765e4e210e5bca2", size = 23032376, upload-time = "2025-05-26T11:18:00.996Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/4d/7dd5c2ffbb960930452a031dc8410746183c924580f2ab4e68ceb5b3043f/jupyterlab-4.4.3-py3-none-any.whl", hash = "sha256:164302f6d4b6c44773dfc38d585665a4db401a16e5296c37df5cba63904fbdea", size = 12295480, upload-time = "2025-05-26T11:17:56.607Z" }, +] + +[[package]] +name = "jupyterlab-pygments" +version = "0.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/90/51/9187be60d989df97f5f0aba133fa54e7300f17616e065d1ada7d7646b6d6/jupyterlab_pygments-0.3.0.tar.gz", hash = "sha256:721aca4d9029252b11cfa9d185e5b5af4d54772bb8072f9b7036f4170054d35d", size = 512900, upload-time = "2023-11-23T09:26:37.44Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b1/dd/ead9d8ea85bf202d90cc513b533f9c363121c7792674f78e0d8a854b63b4/jupyterlab_pygments-0.3.0-py3-none-any.whl", hash = "sha256:841a89020971da1d8693f1a99997aefc5dc424bb1b251fd6322462a1b8842780", size = 15884, upload-time = "2023-11-23T09:26:34.325Z" }, +] + +[[package]] +name = "jupyterlab-server" +version = "2.27.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "babel" }, + { name = "jinja2" }, + { name = "json5" }, + { name = "jsonschema" }, + { name = "jupyter-server" }, + { name = "packaging" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0a/c9/a883ce65eb27905ce77ace410d83587c82ea64dc85a48d1f7ed52bcfa68d/jupyterlab_server-2.27.3.tar.gz", hash = "sha256:eb36caca59e74471988f0ae25c77945610b887f777255aa21f8065def9e51ed4", size = 76173, upload-time = "2024-07-16T17:02:04.149Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/09/2032e7d15c544a0e3cd831c51d77a8ca57f7555b2e1b2922142eddb02a84/jupyterlab_server-2.27.3-py3-none-any.whl", hash = "sha256:e697488f66c3db49df675158a77b3b017520d772c6e1548c7d9bcc5df7944ee4", size = 59700, upload-time = "2024-07-16T17:02:01.115Z" }, +] + +[[package]] +name = "jupyterlab-widgets" +version = "3.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b9/7d/160595ca88ee87ac6ba95d82177d29ec60aaa63821d3077babb22ce031a5/jupyterlab_widgets-3.0.15.tar.gz", hash = "sha256:2920888a0c2922351a9202817957a68c07d99673504d6cd37345299e971bb08b", size = 213149, upload-time = "2025-05-05T12:32:31.004Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/6a/ca128561b22b60bd5a0c4ea26649e68c8556b82bc70a0c396eebc977fe86/jupyterlab_widgets-3.0.15-py3-none-any.whl", hash = "sha256:d59023d7d7ef71400d51e6fee9a88867f6e65e10a4201605d2d7f3e8f012a31c", size = 216571, upload-time = "2025-05-05T12:32:29.534Z" }, +] + +[[package]] +name = "markdown" +version = "3.8.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/c2/4ab49206c17f75cb08d6311171f2d65798988db4360c4d1485bd0eedd67c/markdown-3.8.2.tar.gz", hash = "sha256:247b9a70dd12e27f67431ce62523e675b866d254f900c4fe75ce3dda62237c45", size = 362071, upload-time = "2025-06-19T17:12:44.483Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/96/2b/34cc11786bc00d0f04d0f5fdc3a2b1ae0b6239eef72d3d345805f9ad92a1/markdown-3.8.2-py3-none-any.whl", hash = "sha256:5c83764dbd4e00bdd94d85a19b8d55ccca20fe35b2e678a1422b380324dd5f24", size = 106827, upload-time = "2025-06-19T17:12:42.994Z" }, +] + +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[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/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-inline" +version = "0.1.7" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/99/5b/a36a337438a14116b16480db471ad061c36c3694df7c2084a0da7ba538b7/matplotlib_inline-0.1.7.tar.gz", hash = "sha256:8423b23ec666be3d16e16b60bdd8ac4e86e840ebd1dd11a30b9f117f2fa0ab90", size = 8159, upload-time = "2024-04-15T13:44:44.803Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, +] + +[[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 = "mistune" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/c4/79/bda47f7dd7c3c55770478d6d02c9960c430b0cf1773b72366ff89126ea31/mistune-3.1.3.tar.gz", hash = "sha256:a7035c21782b2becb6be62f8f25d3df81ccb4d6fa477a6525b15af06539f02a0", size = 94347, upload-time = "2025-03-19T14:27:24.955Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/01/4d/23c4e4f09da849e127e9f123241946c23c1e30f45a88366879e064211815/mistune-3.1.3-py3-none-any.whl", hash = "sha256:1a32314113cff28aa6432e99e522677c8587fd83e3d51c29b82a52409c842bd9", size = 53410, upload-time = "2025-03-19T14:27:23.451Z" }, +] + +[[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 = "nbclient" +version = "0.10.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-client" }, + { name = "jupyter-core" }, + { name = "nbformat" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/87/66/7ffd18d58eae90d5721f9f39212327695b749e23ad44b3881744eaf4d9e8/nbclient-0.10.2.tar.gz", hash = "sha256:90b7fc6b810630db87a6d0c2250b1f0ab4cf4d3c27a299b0cde78a4ed3fd9193", size = 62424, upload-time = "2024-12-19T10:32:27.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/34/6d/e7fa07f03a4a7b221d94b4d586edb754a9b0dc3c9e2c93353e9fa4e0d117/nbclient-0.10.2-py3-none-any.whl", hash = "sha256:4ffee11e788b4a27fabeb7955547e4318a5298f34342a4bfd01f2e1faaeadc3d", size = 25434, upload-time = "2024-12-19T10:32:24.139Z" }, +] + +[[package]] +name = "nbconvert" +version = "7.16.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "beautifulsoup4" }, + { name = "bleach", extra = ["css"] }, + { name = "defusedxml" }, + { name = "jinja2" }, + { name = "jupyter-core" }, + { name = "jupyterlab-pygments" }, + { name = "markupsafe" }, + { name = "mistune" }, + { name = "nbclient" }, + { name = "nbformat" }, + { name = "packaging" }, + { name = "pandocfilters" }, + { name = "pygments" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a3/59/f28e15fc47ffb73af68a8d9b47367a8630d76e97ae85ad18271b9db96fdf/nbconvert-7.16.6.tar.gz", hash = "sha256:576a7e37c6480da7b8465eefa66c17844243816ce1ccc372633c6b71c3c0f582", size = 857715, upload-time = "2025-01-28T09:29:14.724Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cc/9a/cd673b2f773a12c992f41309ef81b99da1690426bd2f96957a7ade0d3ed7/nbconvert-7.16.6-py3-none-any.whl", hash = "sha256:1375a7b67e0c2883678c48e506dc320febb57685e5ee67faa51b18a90f3a712b", size = 258525, upload-time = "2025-01-28T09:29:12.551Z" }, +] + +[[package]] +name = "nbformat" +version = "5.10.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastjsonschema" }, + { name = "jsonschema" }, + { name = "jupyter-core" }, + { name = "traitlets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/6d/fd/91545e604bc3dad7dca9ed03284086039b294c6b3d75c0d2fa45f9e9caf3/nbformat-5.10.4.tar.gz", hash = "sha256:322168b14f937a5d11362988ecac2a4952d3d8e3a2cbeb2319584631226d5b3a", size = 142749, upload-time = "2024-04-04T11:20:37.371Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a9/82/0340caa499416c78e5d8f5f05947ae4bc3cba53c9f038ab6e9ed964e22f1/nbformat-5.10.4-py3-none-any.whl", hash = "sha256:3b48d6c8fbca4b299bf3982ea7db1af21580e4fec269ad087b9e81588891200b", size = 78454, upload-time = "2024-04-04T11:20:34.895Z" }, +] + +[[package]] +name = "nest-asyncio" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/83/f8/51569ac65d696c8ecbee95938f89d4abf00f47d58d48f6fbabfe8f0baefe/nest_asyncio-1.6.0.tar.gz", hash = "sha256:6f172d5449aca15afd6c646851f4e31e02c598d553a667e38cafa997cfec55fe", size = 7418, upload-time = "2024-01-21T14:25:19.227Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a0/c4/c2971a3ba4c6103a3d10c4b0f24f461ddc027f0f09763220cf35ca1401b3/nest_asyncio-1.6.0-py3-none-any.whl", hash = "sha256:87af6efd6b5e897c81050477ef65c62e2b2f35d51703cae01aff2905b1852e1c", size = 5195, upload-time = "2024-01-21T14:25:17.223Z" }, +] + +[[package]] +name = "networkx" +version = "3.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/4f/ccdb8ad3a38e583f214547fd2f7ff1fc160c43a75af88e6aec213404b96a/networkx-3.5.tar.gz", hash = "sha256:d4c6f9cf81f52d69230866796b82afbccdec3db7ae4fbd1b65ea750feed50037", size = 2471065, upload-time = "2025-05-29T11:35:07.804Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/8d/776adee7bbf76365fdd7f2552710282c79a4ead5d2a46408c9043a2b70ba/networkx-3.5-py3-none-any.whl", hash = "sha256:0030d386a9a06dee3565298b4a734b68589749a544acbb6c412dc9e2489ec6ec", size = 2034406, upload-time = "2025-05-29T11:35:04.961Z" }, +] + +[[package]] +name = "notebook" +version = "7.4.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, + { name = "jupyterlab" }, + { name = "jupyterlab-server" }, + { name = "notebook-shim" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dc/21/4f83b15e483da4f4f63928edd0cb08b6e7d33f8a15c23b116a90c44c6235/notebook-7.4.3.tar.gz", hash = "sha256:a1567481cd3853f2610ee0ecf5dfa12bb508e878ee8f92152c134ef7f0568a76", size = 13881668, upload-time = "2025-05-26T14:27:21.656Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/76/1b/16c809d799e3ddd7a97c8b43734f79624b74ddef9707e7d92275a13777bc/notebook-7.4.3-py3-none-any.whl", hash = "sha256:9cdeee954e04101cadb195d90e2ab62b7c9286c1d4f858bf3bb54e40df16c0c3", size = 14286402, upload-time = "2025-05-26T14:27:17.339Z" }, +] + +[[package]] +name = "notebook-shim" +version = "0.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "jupyter-server" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/54/d2/92fa3243712b9a3e8bafaf60aac366da1cada3639ca767ff4b5b3654ec28/notebook_shim-0.2.4.tar.gz", hash = "sha256:b4b2cfa1b65d98307ca24361f5b30fe785b53c3fd07b7a47e89acb5e6ac638cb", size = 13167, upload-time = "2024-02-14T23:35:18.353Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/33/bd5b9137445ea4b680023eb0469b2bb969d61303dedb2aac6560ff3d14a1/notebook_shim-0.2.4-py3-none-any.whl", hash = "sha256:411a5be4e9dc882a074ccbcae671eda64cceb068767e9a3419096986560e1cef", size = 13307, upload-time = "2024-02-14T23:35:16.286Z" }, +] + +[[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/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" }, +] + +[[package]] +name = "nvidia-cublas-cu12" +version = "12.6.4.1" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/af/eb/ff4b8c503fa1f1796679dce648854d58751982426e4e4b37d6fce49d259c/nvidia_cublas_cu12-12.6.4.1-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08ed2686e9875d01b58e3cb379c6896df8e76c75e0d4a7f7dace3d7b6d9ef8eb", size = 393138322, upload-time = "2024-11-20T17:40:25.65Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti-cu12" +version = "12.6.80" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/49/60/7b6497946d74bcf1de852a21824d63baad12cd417db4195fc1bfe59db953/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6768bad6cab4f19e8292125e5f1ac8aa7d1718704012a0e3272a6f61c4bce132", size = 8917980, upload-time = "2024-11-20T17:36:04.019Z" }, + { url = "https://files.pythonhosted.org/packages/a5/24/120ee57b218d9952c379d1e026c4479c9ece9997a4fb46303611ee48f038/nvidia_cuda_cupti_cu12-12.6.80-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a3eff6cdfcc6a4c35db968a06fcadb061cbc7d6dde548609a941ff8701b98b73", size = 8917972, upload-time = "2024-10-01T16:58:06.036Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/2e/46030320b5a80661e88039f59060d1790298b4718944a65a7f2aeda3d9e9/nvidia_cuda_nvrtc_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:35b0cc6ee3a9636d5409133e79273ce1f3fd087abb0532d2d2e8fff1fe9efc53", size = 23650380, upload-time = "2024-10-01T17:00:14.643Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/23/e717c5ac26d26cf39a27fbc076240fad2e3b817e5889d671b67f4f9f49c5/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ba3b56a4f896141e25e19ab287cd71e52a6a0f4b29d0d31609f60e3b4d5219b7", size = 897690, upload-time = "2024-11-20T17:35:30.697Z" }, + { url = "https://files.pythonhosted.org/packages/f0/62/65c05e161eeddbafeca24dc461f47de550d9fa8a7e04eb213e32b55cfd99/nvidia_cuda_runtime_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:a84d15d5e1da416dd4774cb42edf5e954a3e60cc945698dc1d5be02321c44dc8", size = 897678, upload-time = "2024-10-01T16:57:33.821Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu12" +version = "9.5.1.17" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/78/4535c9c7f859a64781e43c969a3a7e84c54634e319a996d43ef32ce46f83/nvidia_cudnn_cu12-9.5.1.17-py3-none-manylinux_2_28_x86_64.whl", hash = "sha256:30ac3869f6db17d170e0e556dd6cc5eee02647abc31ca856634d5a40f82c15b2", size = 570988386, upload-time = "2024-10-25T19:54:26.39Z" }, +] + +[[package]] +name = "nvidia-cufft-cu12" +version = "11.3.0.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8f/16/73727675941ab8e6ffd86ca3a4b7b47065edcca7a997920b831f8147c99d/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ccba62eb9cef5559abd5e0d54ceed2d9934030f51163df018532142a8ec533e5", size = 200221632, upload-time = "2024-11-20T17:41:32.357Z" }, + { url = "https://files.pythonhosted.org/packages/60/de/99ec247a07ea40c969d904fc14f3a356b3e2a704121675b75c366b694ee1/nvidia_cufft_cu12-11.3.0.4-py3-none-manylinux2014_x86_64.whl", hash = "sha256:768160ac89f6f7b459bee747e8d175dbf53619cfe74b2a5636264163138013ca", size = 200221622, upload-time = "2024-10-01T17:03:58.79Z" }, +] + +[[package]] +name = "nvidia-cufile-cu12" +version = "1.11.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/66/cc9876340ac68ae71b15c743ddb13f8b30d5244af344ec8322b449e35426/nvidia_cufile_cu12-1.11.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:cc23469d1c7e52ce6c1d55253273d32c565dd22068647f3aa59b3c6b005bf159", size = 1142103, upload-time = "2024-11-20T17:42:11.83Z" }, +] + +[[package]] +name = "nvidia-curand-cu12" +version = "10.3.7.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/1b/44a01c4e70933637c93e6e1a8063d1e998b50213a6b65ac5a9169c47e98e/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a42cd1344297f70b9e39a1e4f467a4e1c10f1da54ff7a85c12197f6c652c8bdf", size = 56279010, upload-time = "2024-11-20T17:42:50.958Z" }, + { url = "https://files.pythonhosted.org/packages/4a/aa/2c7ff0b5ee02eaef890c0ce7d4f74bc30901871c5e45dee1ae6d0083cd80/nvidia_curand_cu12-10.3.7.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:99f1a32f1ac2bd134897fc7a203f779303261268a65762a623bf30cc9fe79117", size = 56279000, upload-time = "2024-10-01T17:04:45.274Z" }, +] + +[[package]] +name = "nvidia-cusolver-cu12" +version = "11.7.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas-cu12" }, + { name = "nvidia-cusparse-cu12" }, + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f0/6e/c2cf12c9ff8b872e92b4a5740701e51ff17689c4d726fca91875b07f655d/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e9e49843a7707e42022babb9bcfa33c29857a93b88020c4e4434656a655b698c", size = 158229790, upload-time = "2024-11-20T17:43:43.211Z" }, + { url = "https://files.pythonhosted.org/packages/9f/81/baba53585da791d043c10084cf9553e074548408e04ae884cfe9193bd484/nvidia_cusolver_cu12-11.7.1.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6cf28f17f64107a0c4d7802be5ff5537b2130bfc112f25d5a30df227058ca0e6", size = 158229780, upload-time = "2024-10-01T17:05:39.875Z" }, +] + +[[package]] +name = "nvidia-cusparse-cu12" +version = "12.5.4.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink-cu12" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/06/1e/b8b7c2f4099a37b96af5c9bb158632ea9e5d9d27d7391d7eb8fc45236674/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7556d9eca156e18184b94947ade0fba5bb47d69cec46bf8660fd2c71a4b48b73", size = 216561367, upload-time = "2024-11-20T17:44:54.824Z" }, + { url = "https://files.pythonhosted.org/packages/43/ac/64c4316ba163e8217a99680c7605f779accffc6a4bcd0c778c12948d3707/nvidia_cusparse_cu12-12.5.4.2-py3-none-manylinux2014_x86_64.whl", hash = "sha256:23749a6571191a215cb74d1cdbff4a86e7b19f1200c071b3fcf844a5bea23a2f", size = 216561357, upload-time = "2024-10-01T17:06:29.861Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu12" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3b/9a/72ef35b399b0e183bc2e8f6f558036922d453c4d8237dab26c666a04244b/nvidia_cusparselt_cu12-0.6.3-py3-none-manylinux2014_x86_64.whl", hash = "sha256:e5c8a26c36445dd2e6812f1177978a24e2d37cacce7e090f297a688d1ec44f46", size = 156785796, upload-time = "2024-10-15T21:29:17.709Z" }, +] + +[[package]] +name = "nvidia-nccl-cu12" +version = "2.26.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/67/ca/f42388aed0fddd64ade7493dbba36e1f534d4e6fdbdd355c6a90030ae028/nvidia_nccl_cu12-2.26.2-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:694cf3879a206553cc9d7dbda76b13efaf610fdb70a50cba303de1b0d1530ac6", size = 201319755, upload-time = "2025-03-13T00:29:55.296Z" }, +] + +[[package]] +name = "nvidia-nvjitlink-cu12" +version = "12.6.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/d7/c5383e47c7e9bf1c99d5bd2a8c935af2b6d705ad831a7ec5c97db4d82f4f/nvidia_nvjitlink_cu12-12.6.85-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:eedc36df9e88b682efe4309aa16b5b4e78c2407eac59e8c10a6a47535164369a", size = 19744971, upload-time = "2024-11-20T17:46:53.366Z" }, +] + +[[package]] +name = "nvidia-nvtx-cu12" +version = "12.6.77" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/9a/fff8376f8e3d084cd1530e1ef7b879bb7d6d265620c95c1b322725c694f4/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b90bed3df379fa79afbd21be8e04a0314336b8ae16768b58f2d34cb1d04cd7d2", size = 89276, upload-time = "2024-11-20T17:38:27.621Z" }, + { url = "https://files.pythonhosted.org/packages/9e/4e/0d0c945463719429b7bd21dece907ad0bde437a2ff12b9b12fee94722ab0/nvidia_nvtx_cu12-12.6.77-py3-none-manylinux2014_x86_64.whl", hash = "sha256:6574241a3ec5fdc9334353ab8c479fe75841dbe8f4532a8fc97ce63503330ba1", size = 89265, upload-time = "2024-10-01T17:00:38.172Z" }, +] + +[[package]] +name = "opencv-python" +version = "4.12.0.88" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ac/71/25c98e634b6bdeca4727c7f6d6927b056080668c5008ad3c8fc9e7f8f6ec/opencv-python-4.12.0.88.tar.gz", hash = "sha256:8b738389cede219405f6f3880b851efa3415ccd674752219377353f017d2994d", size = 95373294, upload-time = "2025-07-07T09:20:52.389Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/68/3da40142e7c21e9b1d4e7ddd6c58738feb013203e6e4b803d62cdd9eb96b/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_arm64.whl", hash = "sha256:f9a1f08883257b95a5764bf517a32d75aec325319c8ed0f89739a57fae9e92a5", size = 37877727, upload-time = "2025-07-07T09:13:31.47Z" }, + { url = "https://files.pythonhosted.org/packages/33/7c/042abe49f58d6ee7e1028eefc3334d98ca69b030e3b567fe245a2b28ea6f/opencv_python-4.12.0.88-cp37-abi3-macosx_13_0_x86_64.whl", hash = "sha256:812eb116ad2b4de43ee116fcd8991c3a687f099ada0b04e68f64899c09448e81", size = 57326471, upload-time = "2025-07-07T09:13:41.26Z" }, + { url = "https://files.pythonhosted.org/packages/62/3a/440bd64736cf8116f01f3b7f9f2e111afb2e02beb2ccc08a6458114a6b5d/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:51fd981c7df6af3e8f70b1556696b05224c4e6b6777bdd2a46b3d4fb09de1a92", size = 45887139, upload-time = "2025-07-07T09:13:50.761Z" }, + { url = "https://files.pythonhosted.org/packages/68/1f/795e7f4aa2eacc59afa4fb61a2e35e510d06414dd5a802b51a012d691b37/opencv_python-4.12.0.88-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:092c16da4c5a163a818f120c22c5e4a2f96e0db4f24e659c701f1fe629a690f9", size = 67041680, upload-time = "2025-07-07T09:14:01.995Z" }, + { url = "https://files.pythonhosted.org/packages/02/96/213fea371d3cb2f1d537612a105792aa0a6659fb2665b22cad709a75bd94/opencv_python-4.12.0.88-cp37-abi3-win32.whl", hash = "sha256:ff554d3f725b39878ac6a2e1fa232ec509c36130927afc18a1719ebf4fbf4357", size = 30284131, upload-time = "2025-07-07T09:14:08.819Z" }, + { url = "https://files.pythonhosted.org/packages/fa/80/eb88edc2e2b11cd2dd2e56f1c80b5784d11d6e6b7f04a1145df64df40065/opencv_python-4.12.0.88-cp37-abi3-win_amd64.whl", hash = "sha256:d98edb20aa932fd8ebd276a72627dad9dc097695b3d435a4257557bbb49a79d2", size = 39000307, upload-time = "2025-07-07T09:14:16.641Z" }, +] + +[[package]] +name = "orjson" +version = "3.11.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/be/4d/8df5f83256a809c22c4d6792ce8d43bb503be0fb7a8e4da9025754b09658/orjson-3.11.3.tar.gz", hash = "sha256:1c0603b1d2ffcd43a411d64797a19556ef76958aef1c182f22dc30860152a98a", size = 5482394, upload-time = "2025-08-26T17:46:43.171Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3d/b0/a7edab2a00cdcb2688e1c943401cb3236323e7bfd2839815c6131a3742f4/orjson-3.11.3-cp312-cp312-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:8c752089db84333e36d754c4baf19c0e1437012242048439c7e80eb0e6426e3b", size = 238259, upload-time = "2025-08-26T17:45:15.093Z" }, + { url = "https://files.pythonhosted.org/packages/e1/c6/ff4865a9cc398a07a83342713b5932e4dc3cb4bf4bc04e8f83dedfc0d736/orjson-3.11.3-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:9b8761b6cf04a856eb544acdd82fc594b978f12ac3602d6374a7edb9d86fd2c2", size = 127633, upload-time = "2025-08-26T17:45:16.417Z" }, + { url = "https://files.pythonhosted.org/packages/6e/e6/e00bea2d9472f44fe8794f523e548ce0ad51eb9693cf538a753a27b8bda4/orjson-3.11.3-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8b13974dc8ac6ba22feaa867fc19135a3e01a134b4f7c9c28162fed4d615008a", size = 123061, upload-time = "2025-08-26T17:45:17.673Z" }, + { url = "https://files.pythonhosted.org/packages/54/31/9fbb78b8e1eb3ac605467cb846e1c08d0588506028b37f4ee21f978a51d4/orjson-3.11.3-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f83abab5bacb76d9c821fd5c07728ff224ed0e52d7a71b7b3de822f3df04e15c", size = 127956, upload-time = "2025-08-26T17:45:19.172Z" }, + { url = "https://files.pythonhosted.org/packages/36/88/b0604c22af1eed9f98d709a96302006915cfd724a7ebd27d6dd11c22d80b/orjson-3.11.3-cp312-cp312-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e6fbaf48a744b94091a56c62897b27c31ee2da93d826aa5b207131a1e13d4064", size = 130790, upload-time = "2025-08-26T17:45:20.586Z" }, + { url = "https://files.pythonhosted.org/packages/0e/9d/1c1238ae9fffbfed51ba1e507731b3faaf6b846126a47e9649222b0fd06f/orjson-3.11.3-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:bc779b4f4bba2847d0d2940081a7b6f7b5877e05408ffbb74fa1faf4a136c424", size = 132385, upload-time = "2025-08-26T17:45:22.036Z" }, + { url = "https://files.pythonhosted.org/packages/a3/b5/c06f1b090a1c875f337e21dd71943bc9d84087f7cdf8c6e9086902c34e42/orjson-3.11.3-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:bd4b909ce4c50faa2192da6bb684d9848d4510b736b0611b6ab4020ea6fd2d23", size = 135305, upload-time = "2025-08-26T17:45:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/a0/26/5f028c7d81ad2ebbf84414ba6d6c9cac03f22f5cd0d01eb40fb2d6a06b07/orjson-3.11.3-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:524b765ad888dc5518bbce12c77c2e83dee1ed6b0992c1790cc5fb49bb4b6667", size = 132875, upload-time = "2025-08-26T17:45:25.182Z" }, + { url = "https://files.pythonhosted.org/packages/fe/d4/b8df70d9cfb56e385bf39b4e915298f9ae6c61454c8154a0f5fd7efcd42e/orjson-3.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:84fd82870b97ae3cdcea9d8746e592b6d40e1e4d4527835fc520c588d2ded04f", size = 130940, upload-time = "2025-08-26T17:45:27.209Z" }, + { url = "https://files.pythonhosted.org/packages/da/5e/afe6a052ebc1a4741c792dd96e9f65bf3939d2094e8b356503b68d48f9f5/orjson-3.11.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:fbecb9709111be913ae6879b07bafd4b0785b44c1eb5cac8ac76da048b3885a1", size = 403852, upload-time = "2025-08-26T17:45:28.478Z" }, + { url = "https://files.pythonhosted.org/packages/f8/90/7bbabafeb2ce65915e9247f14a56b29c9334003536009ef5b122783fe67e/orjson-3.11.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:9dba358d55aee552bd868de348f4736ca5a4086d9a62e2bfbbeeb5629fe8b0cc", size = 146293, upload-time = "2025-08-26T17:45:29.86Z" }, + { url = "https://files.pythonhosted.org/packages/27/b3/2d703946447da8b093350570644a663df69448c9d9330e5f1d9cce997f20/orjson-3.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:eabcf2e84f1d7105f84580e03012270c7e97ecb1fb1618bda395061b2a84a049", size = 135470, upload-time = "2025-08-26T17:45:31.243Z" }, + { url = "https://files.pythonhosted.org/packages/38/70/b14dcfae7aff0e379b0119c8a812f8396678919c431efccc8e8a0263e4d9/orjson-3.11.3-cp312-cp312-win32.whl", hash = "sha256:3782d2c60b8116772aea8d9b7905221437fdf53e7277282e8d8b07c220f96cca", size = 136248, upload-time = "2025-08-26T17:45:32.567Z" }, + { url = "https://files.pythonhosted.org/packages/35/b8/9e3127d65de7fff243f7f3e53f59a531bf6bb295ebe5db024c2503cc0726/orjson-3.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:79b44319268af2eaa3e315b92298de9a0067ade6e6003ddaef72f8e0bedb94f1", size = 131437, upload-time = "2025-08-26T17:45:34.949Z" }, + { url = "https://files.pythonhosted.org/packages/51/92/a946e737d4d8a7fd84a606aba96220043dcc7d6988b9e7551f7f6d5ba5ad/orjson-3.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:0e92a4e83341ef79d835ca21b8bd13e27c859e4e9e4d7b63defc6e58462a3710", size = 125978, upload-time = "2025-08-26T17:45:36.422Z" }, + { url = "https://files.pythonhosted.org/packages/fc/79/8932b27293ad35919571f77cb3693b5906cf14f206ef17546052a241fdf6/orjson-3.11.3-cp313-cp313-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:af40c6612fd2a4b00de648aa26d18186cd1322330bd3a3cc52f87c699e995810", size = 238127, upload-time = "2025-08-26T17:45:38.146Z" }, + { url = "https://files.pythonhosted.org/packages/1c/82/cb93cd8cf132cd7643b30b6c5a56a26c4e780c7a145db6f83de977b540ce/orjson-3.11.3-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:9f1587f26c235894c09e8b5b7636a38091a9e6e7fe4531937534749c04face43", size = 127494, upload-time = "2025-08-26T17:45:39.57Z" }, + { url = "https://files.pythonhosted.org/packages/a4/b8/2d9eb181a9b6bb71463a78882bcac1027fd29cf62c38a40cc02fc11d3495/orjson-3.11.3-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:61dcdad16da5bb486d7227a37a2e789c429397793a6955227cedbd7252eb5a27", size = 123017, upload-time = "2025-08-26T17:45:40.876Z" }, + { url = "https://files.pythonhosted.org/packages/b4/14/a0e971e72d03b509190232356d54c0f34507a05050bd026b8db2bf2c192c/orjson-3.11.3-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:11c6d71478e2cbea0a709e8a06365fa63da81da6498a53e4c4f065881d21ae8f", size = 127898, upload-time = "2025-08-26T17:45:42.188Z" }, + { url = "https://files.pythonhosted.org/packages/8e/af/dc74536722b03d65e17042cc30ae586161093e5b1f29bccda24765a6ae47/orjson-3.11.3-cp313-cp313-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff94112e0098470b665cb0ed06efb187154b63649403b8d5e9aedeb482b4548c", size = 130742, upload-time = "2025-08-26T17:45:43.511Z" }, + { url = "https://files.pythonhosted.org/packages/62/e6/7a3b63b6677bce089fe939353cda24a7679825c43a24e49f757805fc0d8a/orjson-3.11.3-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:ae8b756575aaa2a855a75192f356bbda11a89169830e1439cfb1a3e1a6dde7be", size = 132377, upload-time = "2025-08-26T17:45:45.525Z" }, + { url = "https://files.pythonhosted.org/packages/fc/cd/ce2ab93e2e7eaf518f0fd15e3068b8c43216c8a44ed82ac2b79ce5cef72d/orjson-3.11.3-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:c9416cc19a349c167ef76135b2fe40d03cea93680428efee8771f3e9fb66079d", size = 135313, upload-time = "2025-08-26T17:45:46.821Z" }, + { url = "https://files.pythonhosted.org/packages/d0/b4/f98355eff0bd1a38454209bbc73372ce351ba29933cb3e2eba16c04b9448/orjson-3.11.3-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b822caf5b9752bc6f246eb08124c3d12bf2175b66ab74bac2ef3bbf9221ce1b2", size = 132908, upload-time = "2025-08-26T17:45:48.126Z" }, + { url = "https://files.pythonhosted.org/packages/eb/92/8f5182d7bc2a1bed46ed960b61a39af8389f0ad476120cd99e67182bfb6d/orjson-3.11.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:414f71e3bdd5573893bf5ecdf35c32b213ed20aa15536fe2f588f946c318824f", size = 130905, upload-time = "2025-08-26T17:45:49.414Z" }, + { url = "https://files.pythonhosted.org/packages/1a/60/c41ca753ce9ffe3d0f67b9b4c093bdd6e5fdb1bc53064f992f66bb99954d/orjson-3.11.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:828e3149ad8815dc14468f36ab2a4b819237c155ee1370341b91ea4c8672d2ee", size = 403812, upload-time = "2025-08-26T17:45:51.085Z" }, + { url = "https://files.pythonhosted.org/packages/dd/13/e4a4f16d71ce1868860db59092e78782c67082a8f1dc06a3788aef2b41bc/orjson-3.11.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ac9e05f25627ffc714c21f8dfe3a579445a5c392a9c8ae7ba1d0e9fb5333f56e", size = 146277, upload-time = "2025-08-26T17:45:52.851Z" }, + { url = "https://files.pythonhosted.org/packages/8d/8b/bafb7f0afef9344754a3a0597a12442f1b85a048b82108ef2c956f53babd/orjson-3.11.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e44fbe4000bd321d9f3b648ae46e0196d21577cf66ae684a96ff90b1f7c93633", size = 135418, upload-time = "2025-08-26T17:45:54.806Z" }, + { url = "https://files.pythonhosted.org/packages/60/d4/bae8e4f26afb2c23bea69d2f6d566132584d1c3a5fe89ee8c17b718cab67/orjson-3.11.3-cp313-cp313-win32.whl", hash = "sha256:2039b7847ba3eec1f5886e75e6763a16e18c68a63efc4b029ddf994821e2e66b", size = 136216, upload-time = "2025-08-26T17:45:57.182Z" }, + { url = "https://files.pythonhosted.org/packages/88/76/224985d9f127e121c8cad882cea55f0ebe39f97925de040b75ccd4b33999/orjson-3.11.3-cp313-cp313-win_amd64.whl", hash = "sha256:29be5ac4164aa8bdcba5fa0700a3c9c316b411d8ed9d39ef8a882541bd452fae", size = 131362, upload-time = "2025-08-26T17:45:58.56Z" }, + { url = "https://files.pythonhosted.org/packages/e2/cf/0dce7a0be94bd36d1346be5067ed65ded6adb795fdbe3abd234c8d576d01/orjson-3.11.3-cp313-cp313-win_arm64.whl", hash = "sha256:18bd1435cb1f2857ceb59cfb7de6f92593ef7b831ccd1b9bfb28ca530e539dce", size = 125989, upload-time = "2025-08-26T17:45:59.95Z" }, + { url = "https://files.pythonhosted.org/packages/ef/77/d3b1fef1fc6aaeed4cbf3be2b480114035f4df8fa1a99d2dac1d40d6e924/orjson-3.11.3-cp314-cp314-macosx_10_15_x86_64.macosx_11_0_arm64.macosx_10_15_universal2.whl", hash = "sha256:cf4b81227ec86935568c7edd78352a92e97af8da7bd70bdfdaa0d2e0011a1ab4", size = 238115, upload-time = "2025-08-26T17:46:01.669Z" }, + { url = "https://files.pythonhosted.org/packages/e4/6d/468d21d49bb12f900052edcfbf52c292022d0a323d7828dc6376e6319703/orjson-3.11.3-cp314-cp314-macosx_15_0_arm64.whl", hash = "sha256:bc8bc85b81b6ac9fc4dae393a8c159b817f4c2c9dee5d12b773bddb3b95fc07e", size = 127493, upload-time = "2025-08-26T17:46:03.466Z" }, + { url = "https://files.pythonhosted.org/packages/67/46/1e2588700d354aacdf9e12cc2d98131fb8ac6f31ca65997bef3863edb8ff/orjson-3.11.3-cp314-cp314-manylinux_2_34_aarch64.whl", hash = "sha256:88dcfc514cfd1b0de038443c7b3e6a9797ffb1b3674ef1fd14f701a13397f82d", size = 122998, upload-time = "2025-08-26T17:46:04.803Z" }, + { url = "https://files.pythonhosted.org/packages/3b/94/11137c9b6adb3779f1b34fd98be51608a14b430dbc02c6d41134fbba484c/orjson-3.11.3-cp314-cp314-manylinux_2_34_x86_64.whl", hash = "sha256:d61cd543d69715d5fc0a690c7c6f8dcc307bc23abef9738957981885f5f38229", size = 132915, upload-time = "2025-08-26T17:46:06.237Z" }, + { url = "https://files.pythonhosted.org/packages/10/61/dccedcf9e9bcaac09fdabe9eaee0311ca92115699500efbd31950d878833/orjson-3.11.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2b7b153ed90ababadbef5c3eb39549f9476890d339cf47af563aea7e07db2451", size = 130907, upload-time = "2025-08-26T17:46:07.581Z" }, + { url = "https://files.pythonhosted.org/packages/0e/fd/0e935539aa7b08b3ca0f817d73034f7eb506792aae5ecc3b7c6e679cdf5f/orjson-3.11.3-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:7909ae2460f5f494fecbcd10613beafe40381fd0316e35d6acb5f3a05bfda167", size = 403852, upload-time = "2025-08-26T17:46:08.982Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2b/50ae1a5505cd1043379132fdb2adb8a05f37b3e1ebffe94a5073321966fd/orjson-3.11.3-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:2030c01cbf77bc67bee7eef1e7e31ecf28649353987775e3583062c752da0077", size = 146309, upload-time = "2025-08-26T17:46:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/cd/1d/a473c158e380ef6f32753b5f39a69028b25ec5be331c2049a2201bde2e19/orjson-3.11.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:a0169ebd1cbd94b26c7a7ad282cf5c2744fce054133f959e02eb5265deae1872", size = 135424, upload-time = "2025-08-26T17:46:12.386Z" }, + { url = "https://files.pythonhosted.org/packages/da/09/17d9d2b60592890ff7382e591aa1d9afb202a266b180c3d4049b1ec70e4a/orjson-3.11.3-cp314-cp314-win32.whl", hash = "sha256:0c6d7328c200c349e3a4c6d8c83e0a5ad029bdc2d417f234152bf34842d0fc8d", size = 136266, upload-time = "2025-08-26T17:46:13.853Z" }, + { url = "https://files.pythonhosted.org/packages/15/58/358f6846410a6b4958b74734727e582ed971e13d335d6c7ce3e47730493e/orjson-3.11.3-cp314-cp314-win_amd64.whl", hash = "sha256:317bbe2c069bbc757b1a2e4105b64aacd3bc78279b66a6b9e51e846e4809f804", size = 131351, upload-time = "2025-08-26T17:46:15.27Z" }, + { url = "https://files.pythonhosted.org/packages/28/01/d6b274a0635be0468d4dbd9cafe80c47105937a0d42434e805e67cd2ed8b/orjson-3.11.3-cp314-cp314-win_arm64.whl", hash = "sha256:e8f6a7a27d7b7bec81bd5924163e9af03d49bbb63013f107b48eb5d16db711bc", size = 125985, upload-time = "2025-08-26T17:46:16.67Z" }, +] + +[[package]] +name = "overrides" +version = "7.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/36/86/b585f53236dec60aba864e050778b25045f857e17f6e5ea0ae95fe80edd2/overrides-7.7.0.tar.gz", hash = "sha256:55158fa3d93b98cc75299b1e67078ad9003ca27945c76162c1c0766d6f91820a", size = 22812, upload-time = "2024-01-27T21:01:33.423Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2c/ab/fc8290c6a4c722e5514d80f62b2dc4c4df1a68a41d1364e625c35990fcf3/overrides-7.7.0-py3-none-any.whl", hash = "sha256:c7ed9d062f78b8e4c1a7b70bd8796b35ead4d9f510227ef9c5dc7626c60d7e49", size = 17832, upload-time = "2024-01-27T21:01:31.393Z" }, +] + +[[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 = "pandas" +version = "2.3.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "pytz" }, + { name = "tzdata" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/79/8e/0e90233ac205ad182bd6b422532695d2b9414944a280488105d598c70023/pandas-2.3.2.tar.gz", hash = "sha256:ab7b58f8f82706890924ccdfb5f48002b83d2b5a3845976a9fb705d36c34dcdb", size = 4488684, upload-time = "2025-08-21T10:28:29.257Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/db/614c20fb7a85a14828edd23f1c02db58a30abf3ce76f38806155d160313c/pandas-2.3.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fbb977f802156e7a3f829e9d1d5398f6192375a3e2d1a9ee0803e35fe70a2b9", size = 11587652, upload-time = "2025-08-21T10:27:15.888Z" }, + { url = "https://files.pythonhosted.org/packages/99/b0/756e52f6582cade5e746f19bad0517ff27ba9c73404607c0306585c201b3/pandas-2.3.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b9b52693123dd234b7c985c68b709b0b009f4521000d0525f2b95c22f15944b", size = 10717686, upload-time = "2025-08-21T10:27:18.486Z" }, + { url = "https://files.pythonhosted.org/packages/37/4c/dd5ccc1e357abfeee8353123282de17997f90ff67855f86154e5a13b81e5/pandas-2.3.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0bd281310d4f412733f319a5bc552f86d62cddc5f51d2e392c8787335c994175", size = 11278722, upload-time = "2025-08-21T10:27:21.149Z" }, + { url = "https://files.pythonhosted.org/packages/d3/a4/f7edcfa47e0a88cda0be8b068a5bae710bf264f867edfdf7b71584ace362/pandas-2.3.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:96d31a6b4354e3b9b8a2c848af75d31da390657e3ac6f30c05c82068b9ed79b9", size = 11987803, upload-time = "2025-08-21T10:27:23.767Z" }, + { url = "https://files.pythonhosted.org/packages/f6/61/1bce4129f93ab66f1c68b7ed1c12bac6a70b1b56c5dab359c6bbcd480b52/pandas-2.3.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:df4df0b9d02bb873a106971bb85d448378ef14b86ba96f035f50bbd3688456b4", size = 12766345, upload-time = "2025-08-21T10:27:26.6Z" }, + { url = "https://files.pythonhosted.org/packages/8e/46/80d53de70fee835531da3a1dae827a1e76e77a43ad22a8cd0f8142b61587/pandas-2.3.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:213a5adf93d020b74327cb2c1b842884dbdd37f895f42dcc2f09d451d949f811", size = 13439314, upload-time = "2025-08-21T10:27:29.213Z" }, + { url = "https://files.pythonhosted.org/packages/28/30/8114832daff7489f179971dbc1d854109b7f4365a546e3ea75b6516cea95/pandas-2.3.2-cp312-cp312-win_amd64.whl", hash = "sha256:8c13b81a9347eb8c7548f53fd9a4f08d4dfe996836543f805c987bafa03317ae", size = 10983326, upload-time = "2025-08-21T10:27:31.901Z" }, + { url = "https://files.pythonhosted.org/packages/27/64/a2f7bf678af502e16b472527735d168b22b7824e45a4d7e96a4fbb634b59/pandas-2.3.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0c6ecbac99a354a051ef21c5307601093cb9e0f4b1855984a084bfec9302699e", size = 11531061, upload-time = "2025-08-21T10:27:34.647Z" }, + { url = "https://files.pythonhosted.org/packages/54/4c/c3d21b2b7769ef2f4c2b9299fcadd601efa6729f1357a8dbce8dd949ed70/pandas-2.3.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c6f048aa0fd080d6a06cc7e7537c09b53be6642d330ac6f54a600c3ace857ee9", size = 10668666, upload-time = "2025-08-21T10:27:37.203Z" }, + { url = "https://files.pythonhosted.org/packages/50/e2/f775ba76ecfb3424d7f5862620841cf0edb592e9abd2d2a5387d305fe7a8/pandas-2.3.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0064187b80a5be6f2f9c9d6bdde29372468751dfa89f4211a3c5871854cfbf7a", size = 11332835, upload-time = "2025-08-21T10:27:40.188Z" }, + { url = "https://files.pythonhosted.org/packages/8f/52/0634adaace9be2d8cac9ef78f05c47f3a675882e068438b9d7ec7ef0c13f/pandas-2.3.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4ac8c320bded4718b298281339c1a50fb00a6ba78cb2a63521c39bec95b0209b", size = 12057211, upload-time = "2025-08-21T10:27:43.117Z" }, + { url = "https://files.pythonhosted.org/packages/0b/9d/2df913f14b2deb9c748975fdb2491da1a78773debb25abbc7cbc67c6b549/pandas-2.3.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:114c2fe4f4328cf98ce5716d1532f3ab79c5919f95a9cfee81d9140064a2e4d6", size = 12749277, upload-time = "2025-08-21T10:27:45.474Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/da1a2417026bd14d98c236dba88e39837182459d29dcfcea510b2ac9e8a1/pandas-2.3.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:48fa91c4dfb3b2b9bfdb5c24cd3567575f4e13f9636810462ffed8925352be5a", size = 13415256, upload-time = "2025-08-21T10:27:49.885Z" }, + { url = "https://files.pythonhosted.org/packages/22/3c/f2af1ce8840ef648584a6156489636b5692c162771918aa95707c165ad2b/pandas-2.3.2-cp313-cp313-win_amd64.whl", hash = "sha256:12d039facec710f7ba305786837d0225a3444af7bbd9c15c32ca2d40d157ed8b", size = 10982579, upload-time = "2025-08-21T10:28:08.435Z" }, + { url = "https://files.pythonhosted.org/packages/f3/98/8df69c4097a6719e357dc249bf437b8efbde808038268e584421696cbddf/pandas-2.3.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:c624b615ce97864eb588779ed4046186f967374185c047070545253a52ab2d57", size = 12028163, upload-time = "2025-08-21T10:27:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/0e/23/f95cbcbea319f349e10ff90db488b905c6883f03cbabd34f6b03cbc3c044/pandas-2.3.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:0cee69d583b9b128823d9514171cabb6861e09409af805b54459bd0c821a35c2", size = 11391860, upload-time = "2025-08-21T10:27:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/ad/1b/6a984e98c4abee22058aa75bfb8eb90dce58cf8d7296f8bc56c14bc330b0/pandas-2.3.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2319656ed81124982900b4c37f0e0c58c015af9a7bbc62342ba5ad07ace82ba9", size = 11309830, upload-time = "2025-08-21T10:27:56.957Z" }, + { url = "https://files.pythonhosted.org/packages/15/d5/f0486090eb18dd8710bf60afeaf638ba6817047c0c8ae5c6a25598665609/pandas-2.3.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b37205ad6f00d52f16b6d09f406434ba928c1a1966e2771006a9033c736d30d2", size = 11883216, upload-time = "2025-08-21T10:27:59.302Z" }, + { url = "https://files.pythonhosted.org/packages/10/86/692050c119696da19e20245bbd650d8dfca6ceb577da027c3a73c62a047e/pandas-2.3.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:837248b4fc3a9b83b9c6214699a13f069dc13510a6a6d7f9ba33145d2841a012", size = 12699743, upload-time = "2025-08-21T10:28:02.447Z" }, + { url = "https://files.pythonhosted.org/packages/cd/d7/612123674d7b17cf345aad0a10289b2a384bff404e0463a83c4a3a59d205/pandas-2.3.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:d2c3554bd31b731cd6490d94a28f3abb8dd770634a9e06eb6d2911b9827db370", size = 13186141, upload-time = "2025-08-21T10:28:05.377Z" }, +] + +[[package]] +name = "pandocfilters" +version = "1.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/70/6f/3dd4940bbe001c06a65f88e36bad298bc7a0de5036115639926b0c5c0458/pandocfilters-1.5.1.tar.gz", hash = "sha256:002b4a555ee4ebc03f8b66307e287fa492e4a77b4ea14d3f934328297bb4939e", size = 8454, upload-time = "2024-01-18T20:08:13.726Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/af/4fbc8cab944db5d21b7e2a5b8e9211a03a79852b1157e2c102fcc61ac440/pandocfilters-1.5.1-py2.py3-none-any.whl", hash = "sha256:93be382804a9cdb0a7267585f157e5d1731bbe5545a85b268d6f5fe6232de2bc", size = 8663, upload-time = "2024-01-18T20:08:11.28Z" }, +] + +[[package]] +name = "parso" +version = "0.8.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/66/94/68e2e17afaa9169cf6412ab0f28623903be73d1b32e208d9e8e541bb086d/parso-0.8.4.tar.gz", hash = "sha256:eb3a7b58240fb99099a345571deecc0f9540ea5f4dd2fe14c2a99d6b281ab92d", size = 400609, upload-time = "2024-04-05T09:43:55.897Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c6/ac/dac4a63f978e4dcb3c6d3a78c4d8e0192a113d288502a1216950c41b1027/parso-0.8.4-py2.py3-none-any.whl", hash = "sha256:a418670a20291dacd2dddc80c377c5c3791378ee1e8d12bffc35420643d43f18", size = 103650, upload-time = "2024-04-05T09:43:53.299Z" }, +] + +[[package]] +name = "pexpect" +version = "4.9.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/42/92/cc564bf6381ff43ce1f4d06852fc19a2f11d180f23dc32d9588bee2f149d/pexpect-4.9.0.tar.gz", hash = "sha256:ee7d41123f3c9911050ea2c2dac107568dc43b2d3b0c7557a33212c398ead30f", size = 166450, upload-time = "2023-11-25T09:07:26.339Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c3/059298687310d527a58bb01f3b1965787ee3b40dce76752eda8b44e9a2c5/pexpect-4.9.0-py2.py3-none-any.whl", hash = "sha256:7236d1e080e4936be2dc3e326cec0af72acf9212a7e1d060210e70a47e253523", size = 63772, upload-time = "2023-11-25T06:56:14.81Z" }, +] + +[[package]] +name = "pillow" +version = "11.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/af/cb/bb5c01fcd2a69335b86c22142b2bccfc3464087efb7fd382eee5ffc7fdf7/pillow-11.2.1.tar.gz", hash = "sha256:a64dd61998416367b7ef979b73d3a85853ba9bec4c2925f74e588879a58716b6", size = 47026707, upload-time = "2025-04-12T17:50:03.289Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c7/40/052610b15a1b8961f52537cc8326ca6a881408bc2bdad0d852edeb6ed33b/pillow-11.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78afba22027b4accef10dbd5eed84425930ba41b3ea0a86fa8d20baaf19d807f", size = 3190185, upload-time = "2025-04-12T17:48:00.417Z" }, + { url = "https://files.pythonhosted.org/packages/e5/7e/b86dbd35a5f938632093dc40d1682874c33dcfe832558fc80ca56bfcb774/pillow-11.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:78092232a4ab376a35d68c4e6d5e00dfd73454bd12b230420025fbe178ee3b0b", size = 3030306, upload-time = "2025-04-12T17:48:02.391Z" }, + { url = "https://files.pythonhosted.org/packages/a4/5c/467a161f9ed53e5eab51a42923c33051bf8d1a2af4626ac04f5166e58e0c/pillow-11.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:25a5f306095c6780c52e6bbb6109624b95c5b18e40aab1c3041da3e9e0cd3e2d", size = 4416121, upload-time = "2025-04-12T17:48:04.554Z" }, + { url = "https://files.pythonhosted.org/packages/62/73/972b7742e38ae0e2ac76ab137ca6005dcf877480da0d9d61d93b613065b4/pillow-11.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0c7b29dbd4281923a2bfe562acb734cee96bbb129e96e6972d315ed9f232bef4", size = 4501707, upload-time = "2025-04-12T17:48:06.831Z" }, + { url = "https://files.pythonhosted.org/packages/e4/3a/427e4cb0b9e177efbc1a84798ed20498c4f233abde003c06d2650a6d60cb/pillow-11.2.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:3e645b020f3209a0181a418bffe7b4a93171eef6c4ef6cc20980b30bebf17b7d", size = 4522921, upload-time = "2025-04-12T17:48:09.229Z" }, + { url = "https://files.pythonhosted.org/packages/fe/7c/d8b1330458e4d2f3f45d9508796d7caf0c0d3764c00c823d10f6f1a3b76d/pillow-11.2.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:b2dbea1012ccb784a65349f57bbc93730b96e85b42e9bf7b01ef40443db720b4", size = 4612523, upload-time = "2025-04-12T17:48:11.631Z" }, + { url = "https://files.pythonhosted.org/packages/b3/2f/65738384e0b1acf451de5a573d8153fe84103772d139e1e0bdf1596be2ea/pillow-11.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:da3104c57bbd72948d75f6a9389e6727d2ab6333c3617f0a89d72d4940aa0443", size = 4587836, upload-time = "2025-04-12T17:48:13.592Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c5/e795c9f2ddf3debb2dedd0df889f2fe4b053308bb59a3cc02a0cd144d641/pillow-11.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:598174aef4589af795f66f9caab87ba4ff860ce08cd5bb447c6fc553ffee603c", size = 4669390, upload-time = "2025-04-12T17:48:15.938Z" }, + { url = "https://files.pythonhosted.org/packages/96/ae/ca0099a3995976a9fce2f423166f7bff9b12244afdc7520f6ed38911539a/pillow-11.2.1-cp312-cp312-win32.whl", hash = "sha256:1d535df14716e7f8776b9e7fee118576d65572b4aad3ed639be9e4fa88a1cad3", size = 2332309, upload-time = "2025-04-12T17:48:17.885Z" }, + { url = "https://files.pythonhosted.org/packages/7c/18/24bff2ad716257fc03da964c5e8f05d9790a779a8895d6566e493ccf0189/pillow-11.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:14e33b28bf17c7a38eede290f77db7c664e4eb01f7869e37fa98a5aa95978941", size = 2676768, upload-time = "2025-04-12T17:48:19.655Z" }, + { url = "https://files.pythonhosted.org/packages/da/bb/e8d656c9543276517ee40184aaa39dcb41e683bca121022f9323ae11b39d/pillow-11.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:21e1470ac9e5739ff880c211fc3af01e3ae505859392bf65458c224d0bf283eb", size = 2415087, upload-time = "2025-04-12T17:48:21.991Z" }, + { url = "https://files.pythonhosted.org/packages/36/9c/447528ee3776e7ab8897fe33697a7ff3f0475bb490c5ac1456a03dc57956/pillow-11.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fdec757fea0b793056419bca3e9932eb2b0ceec90ef4813ea4c1e072c389eb28", size = 3190098, upload-time = "2025-04-12T17:48:23.915Z" }, + { url = "https://files.pythonhosted.org/packages/b5/09/29d5cd052f7566a63e5b506fac9c60526e9ecc553825551333e1e18a4858/pillow-11.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b0e130705d568e2f43a17bcbe74d90958e8a16263868a12c3e0d9c8162690830", size = 3030166, upload-time = "2025-04-12T17:48:25.738Z" }, + { url = "https://files.pythonhosted.org/packages/71/5d/446ee132ad35e7600652133f9c2840b4799bbd8e4adba881284860da0a36/pillow-11.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bdb5e09068332578214cadd9c05e3d64d99e0e87591be22a324bdbc18925be0", size = 4408674, upload-time = "2025-04-12T17:48:27.908Z" }, + { url = "https://files.pythonhosted.org/packages/69/5f/cbe509c0ddf91cc3a03bbacf40e5c2339c4912d16458fcb797bb47bcb269/pillow-11.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d189ba1bebfbc0c0e529159631ec72bb9e9bc041f01ec6d3233d6d82eb823bc1", size = 4496005, upload-time = "2025-04-12T17:48:29.888Z" }, + { url = "https://files.pythonhosted.org/packages/f9/b3/dd4338d8fb8a5f312021f2977fb8198a1184893f9b00b02b75d565c33b51/pillow-11.2.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:191955c55d8a712fab8934a42bfefbf99dd0b5875078240943f913bb66d46d9f", size = 4518707, upload-time = "2025-04-12T17:48:31.874Z" }, + { url = "https://files.pythonhosted.org/packages/13/eb/2552ecebc0b887f539111c2cd241f538b8ff5891b8903dfe672e997529be/pillow-11.2.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:ad275964d52e2243430472fc5d2c2334b4fc3ff9c16cb0a19254e25efa03a155", size = 4610008, upload-time = "2025-04-12T17:48:34.422Z" }, + { url = "https://files.pythonhosted.org/packages/72/d1/924ce51bea494cb6e7959522d69d7b1c7e74f6821d84c63c3dc430cbbf3b/pillow-11.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:750f96efe0597382660d8b53e90dd1dd44568a8edb51cb7f9d5d918b80d4de14", size = 4585420, upload-time = "2025-04-12T17:48:37.641Z" }, + { url = "https://files.pythonhosted.org/packages/43/ab/8f81312d255d713b99ca37479a4cb4b0f48195e530cdc1611990eb8fd04b/pillow-11.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:fe15238d3798788d00716637b3d4e7bb6bde18b26e5d08335a96e88564a36b6b", size = 4667655, upload-time = "2025-04-12T17:48:39.652Z" }, + { url = "https://files.pythonhosted.org/packages/94/86/8f2e9d2dc3d308dfd137a07fe1cc478df0a23d42a6c4093b087e738e4827/pillow-11.2.1-cp313-cp313-win32.whl", hash = "sha256:3fe735ced9a607fee4f481423a9c36701a39719252a9bb251679635f99d0f7d2", size = 2332329, upload-time = "2025-04-12T17:48:41.765Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ec/1179083b8d6067a613e4d595359b5fdea65d0a3b7ad623fee906e1b3c4d2/pillow-11.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:74ee3d7ecb3f3c05459ba95eed5efa28d6092d751ce9bf20e3e253a4e497e691", size = 2676388, upload-time = "2025-04-12T17:48:43.625Z" }, + { url = "https://files.pythonhosted.org/packages/23/f1/2fc1e1e294de897df39fa8622d829b8828ddad938b0eaea256d65b84dd72/pillow-11.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:5119225c622403afb4b44bad4c1ca6c1f98eed79db8d3bc6e4e160fc6339d66c", size = 2414950, upload-time = "2025-04-12T17:48:45.475Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/c328c48b3f0ead7bab765a84b4977acb29f101d10e4ef57a5e3400447c03/pillow-11.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:8ce2e8411c7aaef53e6bb29fe98f28cd4fbd9a1d9be2eeea434331aac0536b22", size = 3192759, upload-time = "2025-04-12T17:48:47.866Z" }, + { url = "https://files.pythonhosted.org/packages/18/0e/1c68532d833fc8b9f404d3a642991441d9058eccd5606eab31617f29b6d4/pillow-11.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ee66787e095127116d91dea2143db65c7bb1e232f617aa5957c0d9d2a3f23a7", size = 3033284, upload-time = "2025-04-12T17:48:50.189Z" }, + { url = "https://files.pythonhosted.org/packages/b7/cb/6faf3fb1e7705fd2db74e070f3bf6f88693601b0ed8e81049a8266de4754/pillow-11.2.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9622e3b6c1d8b551b6e6f21873bdcc55762b4b2126633014cea1803368a9aa16", size = 4445826, upload-time = "2025-04-12T17:48:52.346Z" }, + { url = "https://files.pythonhosted.org/packages/07/94/8be03d50b70ca47fb434a358919d6a8d6580f282bbb7af7e4aa40103461d/pillow-11.2.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:63b5dff3a68f371ea06025a1a6966c9a1e1ee452fc8020c2cd0ea41b83e9037b", size = 4527329, upload-time = "2025-04-12T17:48:54.403Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a4/bfe78777076dc405e3bd2080bc32da5ab3945b5a25dc5d8acaa9de64a162/pillow-11.2.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:31df6e2d3d8fc99f993fd253e97fae451a8db2e7207acf97859732273e108406", size = 4549049, upload-time = "2025-04-12T17:48:56.383Z" }, + { url = "https://files.pythonhosted.org/packages/65/4d/eaf9068dc687c24979e977ce5677e253624bd8b616b286f543f0c1b91662/pillow-11.2.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:062b7a42d672c45a70fa1f8b43d1d38ff76b63421cbbe7f88146b39e8a558d91", size = 4635408, upload-time = "2025-04-12T17:48:58.782Z" }, + { url = "https://files.pythonhosted.org/packages/1d/26/0fd443365d9c63bc79feb219f97d935cd4b93af28353cba78d8e77b61719/pillow-11.2.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4eb92eca2711ef8be42fd3f67533765d9fd043b8c80db204f16c8ea62ee1a751", size = 4614863, upload-time = "2025-04-12T17:49:00.709Z" }, + { url = "https://files.pythonhosted.org/packages/49/65/dca4d2506be482c2c6641cacdba5c602bc76d8ceb618fd37de855653a419/pillow-11.2.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f91ebf30830a48c825590aede79376cb40f110b387c17ee9bd59932c961044f9", size = 4692938, upload-time = "2025-04-12T17:49:02.946Z" }, + { url = "https://files.pythonhosted.org/packages/b3/92/1ca0c3f09233bd7decf8f7105a1c4e3162fb9142128c74adad0fb361b7eb/pillow-11.2.1-cp313-cp313t-win32.whl", hash = "sha256:e0b55f27f584ed623221cfe995c912c61606be8513bfa0e07d2c674b4516d9dd", size = 2335774, upload-time = "2025-04-12T17:49:04.889Z" }, + { url = "https://files.pythonhosted.org/packages/a5/ac/77525347cb43b83ae905ffe257bbe2cc6fd23acb9796639a1f56aa59d191/pillow-11.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:36d6b82164c39ce5482f649b437382c0fb2395eabc1e2b1702a6deb8ad647d6e", size = 2681895, upload-time = "2025-04-12T17:49:06.635Z" }, + { url = "https://files.pythonhosted.org/packages/67/32/32dc030cfa91ca0fc52baebbba2e009bb001122a1daa8b6a79ad830b38d3/pillow-11.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:225c832a13326e34f212d2072982bb1adb210e0cc0b153e688743018c94a2681", size = 2417234, upload-time = "2025-04-12T17:49:08.399Z" }, +] + +[[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 = "postgrest" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/53/e5/dc16868833511ca9a661b13c7c4b5ebebc3d70da835da755bef3ee787ad3/postgrest-2.20.0.tar.gz", hash = "sha256:ed390913837810f16965018af7d66972e5759c93c85f4efb35607f38eacef523", size = 13965, upload-time = "2025-09-22T19:13:19.273Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/12/71/4852dde3a93fa312adb92ec0c6a66a81b4f9542cba5c165e42c4cd1459bf/postgrest-2.20.0-py3-none-any.whl", hash = "sha256:f02fc7cbe1e090565ec42e2fc7bfddd44e9db8ddcac8a1786b6f2fccbbd28dd9", size = 22152, upload-time = "2025-09-22T19:13:17.705Z" }, +] + +[[package]] +name = "prometheus-client" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/5e/cf/40dde0a2be27cc1eb41e333d1a674a74ce8b8b0457269cc640fd42b07cf7/prometheus_client-0.22.1.tar.gz", hash = "sha256:190f1331e783cf21eb60bca559354e0a4d4378facecf78f5428c39b675d20d28", size = 69746, upload-time = "2025-06-02T14:29:01.152Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/ae/ec06af4fe3ee72d16973474f122541746196aaa16cea6f66d18b963c6177/prometheus_client-0.22.1-py3-none-any.whl", hash = "sha256:cca895342e308174341b2cbf99a56bef291fbc0ef7b9e5412a0f26d653ba7094", size = 58694, upload-time = "2025-06-02T14:29:00.068Z" }, +] + +[[package]] +name = "prompt-toolkit" +version = "3.0.51" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "wcwidth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/6e/9d084c929dfe9e3bfe0c6a47e31f78a25c54627d64a66e884a8bf5474f1c/prompt_toolkit-3.0.51.tar.gz", hash = "sha256:931a162e3b27fc90c86f1b48bb1fb2c528c2761475e57c9c06de13311c7b54ed", size = 428940, upload-time = "2025-04-15T09:18:47.731Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/4f/5249960887b1fbe561d9ff265496d170b55a735b76724f10ef19f9e40716/prompt_toolkit-3.0.51-py3-none-any.whl", hash = "sha256:52742911fde84e2d423e2f9a4cf1de7d7ac4e51958f648d9540e0fb8db077b07", size = 387810, upload-time = "2025-04-15T09:18:44.753Z" }, +] + +[[package]] +name = "protobuf" +version = "6.31.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/f3/b9655a711b32c19720253f6f06326faf90580834e2e83f840472d752bc8b/protobuf-6.31.1.tar.gz", hash = "sha256:d8cac4c982f0b957a4dc73a80e2ea24fab08e679c0de9deb835f4a12d69aca9a", size = 441797, upload-time = "2025-05-28T19:25:54.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/6f/6ab8e4bf962fd5570d3deaa2d5c38f0a363f57b4501047b5ebeb83ab1125/protobuf-6.31.1-cp310-abi3-win32.whl", hash = "sha256:7fa17d5a29c2e04b7d90e5e32388b8bfd0e7107cd8e616feef7ed3fa6bdab5c9", size = 423603, upload-time = "2025-05-28T19:25:41.198Z" }, + { url = "https://files.pythonhosted.org/packages/44/3a/b15c4347dd4bf3a1b0ee882f384623e2063bb5cf9fa9d57990a4f7df2fb6/protobuf-6.31.1-cp310-abi3-win_amd64.whl", hash = "sha256:426f59d2964864a1a366254fa703b8632dcec0790d8862d30034d8245e1cd447", size = 435283, upload-time = "2025-05-28T19:25:44.275Z" }, + { url = "https://files.pythonhosted.org/packages/6a/c9/b9689a2a250264a84e66c46d8862ba788ee7a641cdca39bccf64f59284b7/protobuf-6.31.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:6f1227473dc43d44ed644425268eb7c2e488ae245d51c6866d19fe158e207402", size = 425604, upload-time = "2025-05-28T19:25:45.702Z" }, + { url = "https://files.pythonhosted.org/packages/76/a1/7a5a94032c83375e4fe7e7f56e3976ea6ac90c5e85fac8576409e25c39c3/protobuf-6.31.1-cp39-abi3-manylinux2014_aarch64.whl", hash = "sha256:a40fc12b84c154884d7d4c4ebd675d5b3b5283e155f324049ae396b95ddebc39", size = 322115, upload-time = "2025-05-28T19:25:47.128Z" }, + { url = "https://files.pythonhosted.org/packages/fa/b1/b59d405d64d31999244643d88c45c8241c58f17cc887e73bcb90602327f8/protobuf-6.31.1-cp39-abi3-manylinux2014_x86_64.whl", hash = "sha256:4ee898bf66f7a8b0bd21bce523814e6fbd8c6add948045ce958b73af7e8878c6", size = 321070, upload-time = "2025-05-28T19:25:50.036Z" }, + { url = "https://files.pythonhosted.org/packages/f7/af/ab3c51ab7507a7325e98ffe691d9495ee3d3aa5f589afad65ec920d39821/protobuf-6.31.1-py3-none-any.whl", hash = "sha256:720a6c7e6b77288b85063569baae8536671b39f15cc22037ec7045658d80489e", size = 168724, upload-time = "2025-05-28T19:25:53.926Z" }, +] + +[[package]] +name = "psutil" +version = "7.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2a/80/336820c1ad9286a4ded7e845b2eccfcb27851ab8ac6abece774a6ff4d3de/psutil-7.0.0.tar.gz", hash = "sha256:7be9c3eba38beccb6495ea33afd982a44074b78f28c434a1f51cc07fd315c456", size = 497003, upload-time = "2025-02-13T21:54:07.946Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/e6/2d26234410f8b8abdbf891c9da62bee396583f713fb9f3325a4760875d22/psutil-7.0.0-cp36-abi3-macosx_10_9_x86_64.whl", hash = "sha256:101d71dc322e3cffd7cea0650b09b3d08b8e7c4109dd6809fe452dfd00e58b25", size = 238051, upload-time = "2025-02-13T21:54:12.36Z" }, + { url = "https://files.pythonhosted.org/packages/04/8b/30f930733afe425e3cbfc0e1468a30a18942350c1a8816acfade80c005c4/psutil-7.0.0-cp36-abi3-macosx_11_0_arm64.whl", hash = "sha256:39db632f6bb862eeccf56660871433e111b6ea58f2caea825571951d4b6aa3da", size = 239535, upload-time = "2025-02-13T21:54:16.07Z" }, + { url = "https://files.pythonhosted.org/packages/2a/ed/d362e84620dd22876b55389248e522338ed1bf134a5edd3b8231d7207f6d/psutil-7.0.0-cp36-abi3-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1fcee592b4c6f146991ca55919ea3d1f8926497a713ed7faaf8225e174581e91", size = 275004, upload-time = "2025-02-13T21:54:18.662Z" }, + { url = "https://files.pythonhosted.org/packages/bf/b9/b0eb3f3cbcb734d930fdf839431606844a825b23eaf9a6ab371edac8162c/psutil-7.0.0-cp36-abi3-manylinux_2_12_x86_64.manylinux2010_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b1388a4f6875d7e2aff5c4ca1cc16c545ed41dd8bb596cefea80111db353a34", size = 277986, upload-time = "2025-02-13T21:54:21.811Z" }, + { url = "https://files.pythonhosted.org/packages/eb/a2/709e0fe2f093556c17fbafda93ac032257242cabcc7ff3369e2cb76a97aa/psutil-7.0.0-cp36-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a5f098451abc2828f7dc6b58d44b532b22f2088f4999a937557b603ce72b1993", size = 279544, upload-time = "2025-02-13T21:54:24.68Z" }, + { url = "https://files.pythonhosted.org/packages/50/e6/eecf58810b9d12e6427369784efe814a1eec0f492084ce8eb8f4d89d6d61/psutil-7.0.0-cp37-abi3-win32.whl", hash = "sha256:ba3fcef7523064a6c9da440fc4d6bd07da93ac726b5733c29027d7dc95b39d99", size = 241053, upload-time = "2025-02-13T21:54:34.31Z" }, + { url = "https://files.pythonhosted.org/packages/50/1b/6921afe68c74868b4c9fa424dad3be35b095e16687989ebbb50ce4fceb7c/psutil-7.0.0-cp37-abi3-win_amd64.whl", hash = "sha256:4cf3d4eb1aa9b348dec30105c55cd9b7d4629285735a102beb4441e38db90553", size = 244885, upload-time = "2025-02-13T21:54:37.486Z" }, +] + +[[package]] +name = "ptyprocess" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/20/e5/16ff212c1e452235a90aeb09066144d0c5a6a8c0834397e03f5224495c4e/ptyprocess-0.7.0.tar.gz", hash = "sha256:5c5d0a3b48ceee0b48485e0c26037c0acd7d29765ca3fbb5cb3831d347423220", size = 70762, upload-time = "2020-12-28T15:15:30.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/22/a6/858897256d0deac81a172289110f31629fc4cee19b6f01283303e18c8db3/ptyprocess-0.7.0-py2.py3-none-any.whl", hash = "sha256:4b41f3967fce3af57cc7e94b888626c18bf37a083e3651ca8feeb66d492fef35", size = 13993, upload-time = "2020-12-28T15:15:28.35Z" }, +] + +[[package]] +name = "pure-eval" +version = "0.2.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cd/05/0a34433a064256a578f1783a10da6df098ceaa4a57bbeaa96a6c0352786b/pure_eval-0.2.3.tar.gz", hash = "sha256:5f4e983f40564c576c7c8635ae88db5956bb2229d7e9237d03b3c0b0190eaf42", size = 19752, upload-time = "2024-07-21T12:58:21.801Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8e/37/efad0257dc6e593a18957422533ff0f87ede7c9c6ea010a2177d738fb82f/pure_eval-0.2.3-py3-none-any.whl", hash = "sha256:1db8e35b67b3d218d818ae653e27f06c3aa420901fa7b081ca98cbedc874e0d0", size = 11842, upload-time = "2024-07-21T12:58:20.04Z" }, +] + +[[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.11.9" +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/ff/5d/09a551ba512d7ca404d785072700d3f6727a02f6f3c24ecfd081c7cf0aa8/pydantic-2.11.9.tar.gz", hash = "sha256:6b8ffda597a14812a7975c90b82a8a2e777d9257aba3453f973acd3c032a18e2", size = 788495, upload-time = "2025-09-13T11:26:39.325Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3e/d3/108f2006987c58e76691d5ae5d200dd3e0f532cb4e5fa3560751c3a1feba/pydantic-2.11.9-py3-none-any.whl", hash = "sha256:c42dd626f5cfc1c6950ce6205ea58c93efa406da65f479dcb4029d5934857da2", size = 444855, upload-time = "2025-09-13T11:26:36.909Z" }, +] + +[[package]] +name = "pydantic-core" +version = "2.33.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ad/88/5f2260bdfae97aabf98f1778d43f69574390ad787afb646292a638c923d4/pydantic_core-2.33.2.tar.gz", hash = "sha256:7cb8bc3605c29176e1b105350d2e6474142d7c1bd1d9327c4a9bdb46bf827acc", size = 435195, upload-time = "2025-04-23T18:33:52.104Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/18/8a/2b41c97f554ec8c71f2a8a5f85cb56a8b0956addfe8b0efb5b3d77e8bdc3/pydantic_core-2.33.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a7ec89dc587667f22b6a0b6579c249fca9026ce7c333fc142ba42411fa243cdc", size = 2009000, upload-time = "2025-04-23T18:31:25.863Z" }, + { url = "https://files.pythonhosted.org/packages/a1/02/6224312aacb3c8ecbaa959897af57181fb6cf3a3d7917fd44d0f2917e6f2/pydantic_core-2.33.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3c6db6e52c6d70aa0d00d45cdb9b40f0433b96380071ea80b09277dba021ddf7", size = 1847996, upload-time = "2025-04-23T18:31:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/d6/46/6dcdf084a523dbe0a0be59d054734b86a981726f221f4562aed313dbcb49/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e61206137cbc65e6d5256e1166f88331d3b6238e082d9f74613b9b765fb9025", size = 1880957, upload-time = "2025-04-23T18:31:28.956Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6b/1ec2c03837ac00886ba8160ce041ce4e325b41d06a034adbef11339ae422/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb8c529b2819c37140eb51b914153063d27ed88e3bdc31b71198a198e921e011", size = 1964199, upload-time = "2025-04-23T18:31:31.025Z" }, + { url = "https://files.pythonhosted.org/packages/2d/1d/6bf34d6adb9debd9136bd197ca72642203ce9aaaa85cfcbfcf20f9696e83/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:c52b02ad8b4e2cf14ca7b3d918f3eb0ee91e63b3167c32591e57c4317e134f8f", size = 2120296, upload-time = "2025-04-23T18:31:32.514Z" }, + { url = "https://files.pythonhosted.org/packages/e0/94/2bd0aaf5a591e974b32a9f7123f16637776c304471a0ab33cf263cf5591a/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:96081f1605125ba0855dfda83f6f3df5ec90c61195421ba72223de35ccfb2f88", size = 2676109, upload-time = "2025-04-23T18:31:33.958Z" }, + { url = "https://files.pythonhosted.org/packages/f9/41/4b043778cf9c4285d59742281a769eac371b9e47e35f98ad321349cc5d61/pydantic_core-2.33.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8f57a69461af2a5fa6e6bbd7a5f60d3b7e6cebb687f55106933188e79ad155c1", size = 2002028, upload-time = "2025-04-23T18:31:39.095Z" }, + { url = "https://files.pythonhosted.org/packages/cb/d5/7bb781bf2748ce3d03af04d5c969fa1308880e1dca35a9bd94e1a96a922e/pydantic_core-2.33.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:572c7e6c8bb4774d2ac88929e3d1f12bc45714ae5ee6d9a788a9fb35e60bb04b", size = 2100044, upload-time = "2025-04-23T18:31:41.034Z" }, + { url = "https://files.pythonhosted.org/packages/fe/36/def5e53e1eb0ad896785702a5bbfd25eed546cdcf4087ad285021a90ed53/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:db4b41f9bd95fbe5acd76d89920336ba96f03e149097365afe1cb092fceb89a1", size = 2058881, upload-time = "2025-04-23T18:31:42.757Z" }, + { url = "https://files.pythonhosted.org/packages/01/6c/57f8d70b2ee57fc3dc8b9610315949837fa8c11d86927b9bb044f8705419/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:fa854f5cf7e33842a892e5c73f45327760bc7bc516339fda888c75ae60edaeb6", size = 2227034, upload-time = "2025-04-23T18:31:44.304Z" }, + { url = "https://files.pythonhosted.org/packages/27/b9/9c17f0396a82b3d5cbea4c24d742083422639e7bb1d5bf600e12cb176a13/pydantic_core-2.33.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:5f483cfb75ff703095c59e365360cb73e00185e01aaea067cd19acffd2ab20ea", size = 2234187, upload-time = "2025-04-23T18:31:45.891Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6a/adf5734ffd52bf86d865093ad70b2ce543415e0e356f6cacabbc0d9ad910/pydantic_core-2.33.2-cp312-cp312-win32.whl", hash = "sha256:9cb1da0f5a471435a7bc7e439b8a728e8b61e59784b2af70d7c169f8dd8ae290", size = 1892628, upload-time = "2025-04-23T18:31:47.819Z" }, + { url = "https://files.pythonhosted.org/packages/43/e4/5479fecb3606c1368d496a825d8411e126133c41224c1e7238be58b87d7e/pydantic_core-2.33.2-cp312-cp312-win_amd64.whl", hash = "sha256:f941635f2a3d96b2973e867144fde513665c87f13fe0e193c158ac51bfaaa7b2", size = 1955866, upload-time = "2025-04-23T18:31:49.635Z" }, + { url = "https://files.pythonhosted.org/packages/0d/24/8b11e8b3e2be9dd82df4b11408a67c61bb4dc4f8e11b5b0fc888b38118b5/pydantic_core-2.33.2-cp312-cp312-win_arm64.whl", hash = "sha256:cca3868ddfaccfbc4bfb1d608e2ccaaebe0ae628e1416aeb9c4d88c001bb45ab", size = 1888894, upload-time = "2025-04-23T18:31:51.609Z" }, + { url = "https://files.pythonhosted.org/packages/46/8c/99040727b41f56616573a28771b1bfa08a3d3fe74d3d513f01251f79f172/pydantic_core-2.33.2-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:1082dd3e2d7109ad8b7da48e1d4710c8d06c253cbc4a27c1cff4fbcaa97a9e3f", size = 2015688, upload-time = "2025-04-23T18:31:53.175Z" }, + { url = "https://files.pythonhosted.org/packages/3a/cc/5999d1eb705a6cefc31f0b4a90e9f7fc400539b1a1030529700cc1b51838/pydantic_core-2.33.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f517ca031dfc037a9c07e748cefd8d96235088b83b4f4ba8939105d20fa1dcd6", size = 1844808, upload-time = "2025-04-23T18:31:54.79Z" }, + { url = "https://files.pythonhosted.org/packages/6f/5e/a0a7b8885c98889a18b6e376f344da1ef323d270b44edf8174d6bce4d622/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0a9f2c9dd19656823cb8250b0724ee9c60a82f3cdf68a080979d13092a3b0fef", size = 1885580, upload-time = "2025-04-23T18:31:57.393Z" }, + { url = "https://files.pythonhosted.org/packages/3b/2a/953581f343c7d11a304581156618c3f592435523dd9d79865903272c256a/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:2b0a451c263b01acebe51895bfb0e1cc842a5c666efe06cdf13846c7418caa9a", size = 1973859, upload-time = "2025-04-23T18:31:59.065Z" }, + { url = "https://files.pythonhosted.org/packages/e6/55/f1a813904771c03a3f97f676c62cca0c0a4138654107c1b61f19c644868b/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1ea40a64d23faa25e62a70ad163571c0b342b8bf66d5fa612ac0dec4f069d916", size = 2120810, upload-time = "2025-04-23T18:32:00.78Z" }, + { url = "https://files.pythonhosted.org/packages/aa/c3/053389835a996e18853ba107a63caae0b9deb4a276c6b472931ea9ae6e48/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0fb2d542b4d66f9470e8065c5469ec676978d625a8b7a363f07d9a501a9cb36a", size = 2676498, upload-time = "2025-04-23T18:32:02.418Z" }, + { url = "https://files.pythonhosted.org/packages/eb/3c/f4abd740877a35abade05e437245b192f9d0ffb48bbbbd708df33d3cda37/pydantic_core-2.33.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9fdac5d6ffa1b5a83bca06ffe7583f5576555e6c8b3a91fbd25ea7780f825f7d", size = 2000611, upload-time = "2025-04-23T18:32:04.152Z" }, + { url = "https://files.pythonhosted.org/packages/59/a7/63ef2fed1837d1121a894d0ce88439fe3e3b3e48c7543b2a4479eb99c2bd/pydantic_core-2.33.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:04a1a413977ab517154eebb2d326da71638271477d6ad87a769102f7c2488c56", size = 2107924, upload-time = "2025-04-23T18:32:06.129Z" }, + { url = "https://files.pythonhosted.org/packages/04/8f/2551964ef045669801675f1cfc3b0d74147f4901c3ffa42be2ddb1f0efc4/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:c8e7af2f4e0194c22b5b37205bfb293d166a7344a5b0d0eaccebc376546d77d5", size = 2063196, upload-time = "2025-04-23T18:32:08.178Z" }, + { url = "https://files.pythonhosted.org/packages/26/bd/d9602777e77fc6dbb0c7db9ad356e9a985825547dce5ad1d30ee04903918/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:5c92edd15cd58b3c2d34873597a1e20f13094f59cf88068adb18947df5455b4e", size = 2236389, upload-time = "2025-04-23T18:32:10.242Z" }, + { url = "https://files.pythonhosted.org/packages/42/db/0e950daa7e2230423ab342ae918a794964b053bec24ba8af013fc7c94846/pydantic_core-2.33.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:65132b7b4a1c0beded5e057324b7e16e10910c106d43675d9bd87d4f38dde162", size = 2239223, upload-time = "2025-04-23T18:32:12.382Z" }, + { url = "https://files.pythonhosted.org/packages/58/4d/4f937099c545a8a17eb52cb67fe0447fd9a373b348ccfa9a87f141eeb00f/pydantic_core-2.33.2-cp313-cp313-win32.whl", hash = "sha256:52fb90784e0a242bb96ec53f42196a17278855b0f31ac7c3cc6f5c1ec4811849", size = 1900473, upload-time = "2025-04-23T18:32:14.034Z" }, + { url = "https://files.pythonhosted.org/packages/a0/75/4a0a9bac998d78d889def5e4ef2b065acba8cae8c93696906c3a91f310ca/pydantic_core-2.33.2-cp313-cp313-win_amd64.whl", hash = "sha256:c083a3bdd5a93dfe480f1125926afcdbf2917ae714bdb80b36d34318b2bec5d9", size = 1955269, upload-time = "2025-04-23T18:32:15.783Z" }, + { url = "https://files.pythonhosted.org/packages/f9/86/1beda0576969592f1497b4ce8e7bc8cbdf614c352426271b1b10d5f0aa64/pydantic_core-2.33.2-cp313-cp313-win_arm64.whl", hash = "sha256:e80b087132752f6b3d714f041ccf74403799d3b23a72722ea2e6ba2e892555b9", size = 1893921, upload-time = "2025-04-23T18:32:18.473Z" }, + { url = "https://files.pythonhosted.org/packages/a4/7d/e09391c2eebeab681df2b74bfe6c43422fffede8dc74187b2b0bf6fd7571/pydantic_core-2.33.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:61c18fba8e5e9db3ab908620af374db0ac1baa69f0f32df4f61ae23f15e586ac", size = 1806162, upload-time = "2025-04-23T18:32:20.188Z" }, + { url = "https://files.pythonhosted.org/packages/f1/3d/847b6b1fed9f8ed3bb95a9ad04fbd0b212e832d4f0f50ff4d9ee5a9f15cf/pydantic_core-2.33.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95237e53bb015f67b63c91af7518a62a8660376a6a0db19b89acc77a4d6199f5", size = 1981560, upload-time = "2025-04-23T18:32:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/6f/9a/e73262f6c6656262b5fdd723ad90f518f579b7bc8622e43a942eec53c938/pydantic_core-2.33.2-cp313-cp313t-win_amd64.whl", hash = "sha256:c2fc0a768ef76c15ab9238afa6da7f69895bb5d1ee83aeea2e3509af4472d0b9", size = 1935777, upload-time = "2025-04-23T18:32:25.088Z" }, +] + +[[package]] +name = "pydub" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fe/9a/e6bca0eed82db26562c73b5076539a4a08d3cffd19c3cc5913a3e61145fd/pydub-0.25.1.tar.gz", hash = "sha256:980a33ce9949cab2a569606b65674d748ecbca4f0796887fd6f46173a7b0d30f", size = 38326, upload-time = "2021-03-10T02:09:54.659Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a6/53/d78dc063216e62fc55f6b2eebb447f6a4b0a59f55c8406376f76bf959b08/pydub-0.25.1-py2.py3-none-any.whl", hash = "sha256:65617e33033874b59d87db603aa1ed450633288aefead953b30bded59cb599a6", size = 32327, upload-time = "2021-03-10T02:09:53.503Z" }, +] + +[[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 = "pyjwt" +version = "2.10.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e7/46/bd74733ff231675599650d3e47f361794b22ef3e3770998dda30d3b63726/pyjwt-2.10.1.tar.gz", hash = "sha256:3cc5772eb20009233caf06e9d8a0577824723b44e6648ee0a2aedb6cf9381953", size = 87785, upload-time = "2024-11-28T03:43:29.933Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/61/ad/689f02752eeec26aed679477e80e632ef1b682313be70793d798c1d5fc8f/PyJWT-2.10.1-py3-none-any.whl", hash = "sha256:dcdd193e30abefd5debf142f9adfcdd2b58004e644f25406ffaebd50bd98dacb", size = 22997, upload-time = "2024-11-28T03:43:27.893Z" }, +] + +[package.optional-dependencies] +crypto = [ + { name = "cryptography", version = "45.0.7", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.14' and platform_python_implementation != 'PyPy'" }, + { name = "cryptography", version = "46.0.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.14' or platform_python_implementation == 'PyPy'" }, +] + +[[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-dotenv" +version = "1.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/b0/4bc07ccd3572a2f9df7e6782f52b0c6c90dcbb803ac4a167702d7d0dfe1e/python_dotenv-1.1.1.tar.gz", hash = "sha256:a8a6399716257f45be6a007360200409fce5cda2661e3dec71d23dc15f6189ab", size = 41978, upload-time = "2025-06-24T04:21:07.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5f/ed/539768cf28c661b5b068d66d96a2f155c4971a5d55684a514c1a0e0dec2f/python_dotenv-1.1.1-py3-none-any.whl", hash = "sha256:31f23644fe2602f88ff55e1f5c79ba497e01224ee7737937930c448e4d0e24dc", size = 20556, upload-time = "2025-06-24T04:21:06.073Z" }, +] + +[[package]] +name = "python-json-logger" +version = "3.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/de/d3144a0bceede957f961e975f3752760fbe390d57fbe194baf709d8f1f7b/python_json_logger-3.3.0.tar.gz", hash = "sha256:12b7e74b17775e7d565129296105bbe3910842d9d0eb083fc83a6a617aa8df84", size = 16642, upload-time = "2025-03-07T07:08:27.301Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/08/20/0f2523b9e50a8052bc6a8b732dfc8568abbdc42010aef03a2d750bdab3b2/python_json_logger-3.3.0-py3-none-any.whl", hash = "sha256:dd980fae8cffb24c13caf6e158d3d61c0d6d22342f932cb6e9deedab3d35eec7", size = 15163, upload-time = "2025-03-07T07:08:25.627Z" }, +] + +[[package]] +name = "python-multipart" +version = "0.0.20" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/87/f44d7c9f274c7ee665a29b885ec97089ec5dc034c7f3fafa03da9e39a09e/python_multipart-0.0.20.tar.gz", hash = "sha256:8dd0cab45b8e23064ae09147625994d090fa46f5b0d1e13af944c331a7fa9d13", size = 37158, upload-time = "2024-12-16T19:45:46.972Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/45/58/38b5afbc1a800eeea951b9285d3912613f2603bdf897a4ab0f4bd7f405fc/python_multipart-0.0.20-py3-none-any.whl", hash = "sha256:8a62d3a8335e06589fe01f2a3e178cdcc632f3fbe0d492ad9ee0ec35aab1f104", size = 24546, upload-time = "2024-12-16T19:45:44.423Z" }, +] + +[[package]] +name = "pytz" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f8/bf/abbd3cdfb8fbc7fb3d4d38d320f2441b1e7cbe29be4f23797b4a2b5d8aac/pytz-2025.2.tar.gz", hash = "sha256:360b9e3dbb49a209c21ad61809c7fb453643e048b38924c765813546746e81c3", size = 320884, upload-time = "2025-03-25T02:25:00.538Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/c4/34e93fe5f5429d7570ec1fa436f1986fb1f00c3e0f43a589fe2bbcd22c3f/pytz-2025.2-py2.py3-none-any.whl", hash = "sha256:5ddf76296dd8c44c26eb8f4b6f35488f3ccbf6fbbd7adee0b7262d43f0ec2f00", size = 509225, upload-time = "2025-03-25T02:24:58.468Z" }, +] + +[[package]] +name = "pywin32" +version = "310" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/ec/4fdbe47932f671d6e348474ea35ed94227fb5df56a7c30cbbb42cd396ed0/pywin32-310-cp312-cp312-win32.whl", hash = "sha256:8a75a5cc3893e83a108c05d82198880704c44bbaee4d06e442e471d3c9ea4f3d", size = 8796239, upload-time = "2025-03-17T00:55:58.807Z" }, + { url = "https://files.pythonhosted.org/packages/e3/e5/b0627f8bb84e06991bea89ad8153a9e50ace40b2e1195d68e9dff6b03d0f/pywin32-310-cp312-cp312-win_amd64.whl", hash = "sha256:bf5c397c9a9a19a6f62f3fb821fbf36cac08f03770056711f765ec1503972060", size = 9503839, upload-time = "2025-03-17T00:56:00.8Z" }, + { url = "https://files.pythonhosted.org/packages/1f/32/9ccf53748df72301a89713936645a664ec001abd35ecc8578beda593d37d/pywin32-310-cp312-cp312-win_arm64.whl", hash = "sha256:2349cc906eae872d0663d4d6290d13b90621eaf78964bb1578632ff20e152966", size = 8459470, upload-time = "2025-03-17T00:56:02.601Z" }, + { url = "https://files.pythonhosted.org/packages/1c/09/9c1b978ffc4ae53999e89c19c77ba882d9fce476729f23ef55211ea1c034/pywin32-310-cp313-cp313-win32.whl", hash = "sha256:5d241a659c496ada3253cd01cfaa779b048e90ce4b2b38cd44168ad555ce74ab", size = 8794384, upload-time = "2025-03-17T00:56:04.383Z" }, + { url = "https://files.pythonhosted.org/packages/45/3c/b4640f740ffebadd5d34df35fecba0e1cfef8fde9f3e594df91c28ad9b50/pywin32-310-cp313-cp313-win_amd64.whl", hash = "sha256:667827eb3a90208ddbdcc9e860c81bde63a135710e21e4cb3348968e4bd5249e", size = 9503039, upload-time = "2025-03-17T00:56:06.207Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f4/f785020090fb050e7fb6d34b780f2231f302609dc964672f72bfaeb59a28/pywin32-310-cp313-cp313-win_arm64.whl", hash = "sha256:e308f831de771482b7cf692a1f308f8fca701b2d8f9dde6cc440c7da17e47b33", size = 8458152, upload-time = "2025-03-17T00:56:07.819Z" }, +] + +[[package]] +name = "pywinpty" +version = "2.0.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/7c/917f9c4681bb8d34bfbe0b79d36bbcd902651aeab48790df3d30ba0202fb/pywinpty-2.0.15.tar.gz", hash = "sha256:312cf39153a8736c617d45ce8b6ad6cd2107de121df91c455b10ce6bba7a39b2", size = 29017, upload-time = "2025-02-03T21:53:23.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/e5/9714def18c3a411809771a3fbcec70bffa764b9675afb00048a620fca604/pywinpty-2.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:83a8f20b430bbc5d8957249f875341a60219a4e971580f2ba694fbfb54a45ebc", size = 1405243, upload-time = "2025-02-03T21:56:52.476Z" }, + { url = "https://files.pythonhosted.org/packages/fb/16/2ab7b3b7f55f3c6929e5f629e1a68362981e4e5fed592a2ed1cb4b4914a5/pywinpty-2.0.15-cp313-cp313-win_amd64.whl", hash = "sha256:ab5920877dd632c124b4ed17bc6dd6ef3b9f86cd492b963ffdb1a67b85b0f408", size = 1405020, upload-time = "2025-02-03T21:56:04.753Z" }, + { url = "https://files.pythonhosted.org/packages/7c/16/edef3515dd2030db2795dbfbe392232c7a0f3dc41b98e92b38b42ba497c7/pywinpty-2.0.15-cp313-cp313t-win_amd64.whl", hash = "sha256:a4560ad8c01e537708d2790dbe7da7d986791de805d89dd0d3697ca59e9e4901", size = 1404151, upload-time = "2025-02-03T21:55:53.628Z" }, +] + +[[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/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 = "pyzmq" +version = "27.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cffi", marker = "implementation_name == 'pypy'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f1/06/50a4e9648b3e8b992bef8eb632e457307553a89d294103213cfd47b3da69/pyzmq-27.0.0.tar.gz", hash = "sha256:b1f08eeb9ce1510e6939b6e5dcd46a17765e2333daae78ecf4606808442e52cf", size = 280478, upload-time = "2025-06-13T14:09:07.087Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/a7/9ad68f55b8834ede477842214feba6a4c786d936c022a67625497aacf61d/pyzmq-27.0.0-cp312-abi3-macosx_10_15_universal2.whl", hash = "sha256:cbabc59dcfaac66655c040dfcb8118f133fb5dde185e5fc152628354c1598e52", size = 1305438, upload-time = "2025-06-13T14:07:31.676Z" }, + { url = "https://files.pythonhosted.org/packages/ba/ee/26aa0f98665a22bc90ebe12dced1de5f3eaca05363b717f6fb229b3421b3/pyzmq-27.0.0-cp312-abi3-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:cb0ac5179cba4b2f94f1aa208fbb77b62c4c9bf24dd446278b8b602cf85fcda3", size = 895095, upload-time = "2025-06-13T14:07:33.104Z" }, + { url = "https://files.pythonhosted.org/packages/cf/85/c57e7ab216ecd8aa4cc7e3b83b06cc4e9cf45c87b0afc095f10cd5ce87c1/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:53a48f0228eab6cbf69fde3aa3c03cbe04e50e623ef92ae395fce47ef8a76152", size = 651826, upload-time = "2025-06-13T14:07:34.831Z" }, + { url = "https://files.pythonhosted.org/packages/69/9a/9ea7e230feda9400fb0ae0d61d7d6ddda635e718d941c44eeab22a179d34/pyzmq-27.0.0-cp312-abi3-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:111db5f395e09f7e775f759d598f43cb815fc58e0147623c4816486e1a39dc22", size = 839750, upload-time = "2025-06-13T14:07:36.553Z" }, + { url = "https://files.pythonhosted.org/packages/08/66/4cebfbe71f3dfbd417011daca267539f62ed0fbc68105357b68bbb1a25b7/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:c8878011653dcdc27cc2c57e04ff96f0471e797f5c19ac3d7813a245bcb24371", size = 1641357, upload-time = "2025-06-13T14:07:38.21Z" }, + { url = "https://files.pythonhosted.org/packages/ac/f6/b0f62578c08d2471c791287149cb8c2aaea414ae98c6e995c7dbe008adfb/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_i686.whl", hash = "sha256:c0ed2c1f335ba55b5fdc964622254917d6b782311c50e138863eda409fbb3b6d", size = 2020281, upload-time = "2025-06-13T14:07:39.599Z" }, + { url = "https://files.pythonhosted.org/packages/37/b9/4f670b15c7498495da9159edc374ec09c88a86d9cd5a47d892f69df23450/pyzmq-27.0.0-cp312-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:e918d70862d4cfd4b1c187310015646a14e1f5917922ab45b29f28f345eeb6be", size = 1877110, upload-time = "2025-06-13T14:07:41.027Z" }, + { url = "https://files.pythonhosted.org/packages/66/31/9dee25c226295b740609f0d46db2fe972b23b6f5cf786360980524a3ba92/pyzmq-27.0.0-cp312-abi3-win32.whl", hash = "sha256:88b4e43cab04c3c0f0d55df3b1eef62df2b629a1a369b5289a58f6fa8b07c4f4", size = 559297, upload-time = "2025-06-13T14:07:42.533Z" }, + { url = "https://files.pythonhosted.org/packages/9b/12/52da5509800f7ff2d287b2f2b4e636e7ea0f001181cba6964ff6c1537778/pyzmq-27.0.0-cp312-abi3-win_amd64.whl", hash = "sha256:dce4199bf5f648a902ce37e7b3afa286f305cd2ef7a8b6ec907470ccb6c8b371", size = 619203, upload-time = "2025-06-13T14:07:43.843Z" }, + { url = "https://files.pythonhosted.org/packages/93/6d/7f2e53b19d1edb1eb4f09ec7c3a1f945ca0aac272099eab757d15699202b/pyzmq-27.0.0-cp312-abi3-win_arm64.whl", hash = "sha256:56e46bbb85d52c1072b3f809cc1ce77251d560bc036d3a312b96db1afe76db2e", size = 551927, upload-time = "2025-06-13T14:07:45.51Z" }, + { url = "https://files.pythonhosted.org/packages/19/62/876b27c4ff777db4ceba1c69ea90d3c825bb4f8d5e7cd987ce5802e33c55/pyzmq-27.0.0-cp313-cp313t-macosx_10_15_universal2.whl", hash = "sha256:c36ad534c0c29b4afa088dc53543c525b23c0797e01b69fef59b1a9c0e38b688", size = 1340826, upload-time = "2025-06-13T14:07:46.881Z" }, + { url = "https://files.pythonhosted.org/packages/43/69/58ef8f4f59d3bcd505260c73bee87b008850f45edca40ddaba54273c35f4/pyzmq-27.0.0-cp313-cp313t-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:67855c14173aec36395d7777aaba3cc527b393821f30143fd20b98e1ff31fd38", size = 897283, upload-time = "2025-06-13T14:07:49.562Z" }, + { url = "https://files.pythonhosted.org/packages/43/15/93a0d0396700a60475ad3c5d42c5f1c308d3570bc94626b86c71ef9953e0/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8617c7d43cd8ccdb62aebe984bfed77ca8f036e6c3e46dd3dddda64b10f0ab7a", size = 660567, upload-time = "2025-06-13T14:07:51.364Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b3/fe055513e498ca32f64509abae19b9c9eb4d7c829e02bd8997dd51b029eb/pyzmq-27.0.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:67bfbcbd0a04c575e8103a6061d03e393d9f80ffdb9beb3189261e9e9bc5d5e9", size = 847681, upload-time = "2025-06-13T14:07:52.77Z" }, + { url = "https://files.pythonhosted.org/packages/b6/4f/ff15300b00b5b602191f3df06bbc8dd4164e805fdd65bb77ffbb9c5facdc/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5cd11d46d7b7e5958121b3eaf4cd8638eff3a720ec527692132f05a57f14341d", size = 1650148, upload-time = "2025-06-13T14:07:54.178Z" }, + { url = "https://files.pythonhosted.org/packages/c4/6f/84bdfff2a224a6f26a24249a342e5906993c50b0761e311e81b39aef52a7/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:b801c2e40c5aa6072c2f4876de8dccd100af6d9918d4d0d7aa54a1d982fd4f44", size = 2023768, upload-time = "2025-06-13T14:07:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/64/39/dc2db178c26a42228c5ac94a9cc595030458aa64c8d796a7727947afbf55/pyzmq-27.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:20d5cb29e8c5f76a127c75b6e7a77e846bc4b655c373baa098c26a61b7ecd0ef", size = 1885199, upload-time = "2025-06-13T14:07:57.166Z" }, + { url = "https://files.pythonhosted.org/packages/c7/21/dae7b06a1f8cdee5d8e7a63d99c5d129c401acc40410bef2cbf42025e26f/pyzmq-27.0.0-cp313-cp313t-win32.whl", hash = "sha256:a20528da85c7ac7a19b7384e8c3f8fa707841fd85afc4ed56eda59d93e3d98ad", size = 575439, upload-time = "2025-06-13T14:07:58.959Z" }, + { url = "https://files.pythonhosted.org/packages/eb/bc/1709dc55f0970cf4cb8259e435e6773f9946f41a045c2cb90e870b7072da/pyzmq-27.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:d8229f2efece6a660ee211d74d91dbc2a76b95544d46c74c615e491900dc107f", size = 639933, upload-time = "2025-06-13T14:08:00.777Z" }, +] + +[[package]] +name = "realtime" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pydantic" }, + { name = "typing-extensions" }, + { name = "websockets" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d9/00/b086740c42757d41d61e9c300cdda8188f90d2d0ef1caf41691c3ca57737/realtime-2.20.0.tar.gz", hash = "sha256:969fbfd4bcf4973e5500554c3a46b95e02f2d09753c5141e525fbd0397ee9b0a", size = 18158, upload-time = "2025-09-22T19:13:21.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ab/af/9910f8d55bdacd493c41f9a901bcd5b1892b9a3b1f216b5b751a58cda1df/realtime-2.20.0-py3-none-any.whl", hash = "sha256:42bacbbae6a04a43665812e4904a5f42b2b2a28a34fe0f4b97531d6763f46111", size = 21696, upload-time = "2025-09-22T19:13:20.517Z" }, +] + +[[package]] +name = "referencing" +version = "0.36.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "attrs" }, + { name = "rpds-py" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/2f/db/98b5c277be99dd18bfd91dd04e1b759cad18d1a338188c936e92f921c7e2/referencing-0.36.2.tar.gz", hash = "sha256:df2e89862cd09deabbdba16944cc3f10feb6b3e6f18e902f7cc25609a34775aa", size = 74744, upload-time = "2025-01-25T08:48:16.138Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c1/b1/3baf80dc6d2b7bc27a95a67752d0208e410351e3feb4eb78de5f77454d8d/referencing-0.36.2-py3-none-any.whl", hash = "sha256:e8699adbbf8b5c7de96d8ffa0eb5c158b3beafce084968e2ea8bb08c6794dcd0", size = 26775, upload-time = "2025-01-25T08:48:14.241Z" }, +] + +[[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 = "rfc3339-validator" +version = "0.1.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/ea/a9387748e2d111c3c2b275ba970b735e04e15cdb1eb30693b6b5708c4dbd/rfc3339_validator-0.1.4.tar.gz", hash = "sha256:138a2abdf93304ad60530167e51d2dfb9549521a836871b88d7f4695d0022f6b", size = 5513, upload-time = "2021-05-12T16:37:54.178Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/44/4e421b96b67b2daff264473f7465db72fbdf36a07e05494f50300cc7b0c6/rfc3339_validator-0.1.4-py2.py3-none-any.whl", hash = "sha256:24f6ec1eda14ef823da9e36ec7113124b39c04d50a4d3d3a3c2859577e7791fa", size = 3490, upload-time = "2021-05-12T16:37:52.536Z" }, +] + +[[package]] +name = "rfc3986-validator" +version = "0.1.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/da/88/f270de456dd7d11dcc808abfa291ecdd3f45ff44e3b549ffa01b126464d0/rfc3986_validator-0.1.1.tar.gz", hash = "sha256:3d44bde7921b3b9ec3ae4e3adca370438eccebc676456449b145d533b240d055", size = 6760, upload-time = "2019-10-28T16:00:19.144Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/51/17023c0f8f1869d8806b979a2bffa3f861f26a3f1a66b094288323fba52f/rfc3986_validator-0.1.1-py2.py3-none-any.whl", hash = "sha256:2f235c432ef459970b4306369336b9d5dbdda31b510ca1e327636e01f528bfa9", size = 4242, upload-time = "2019-10-28T16:00:13.976Z" }, +] + +[[package]] +name = "rich" +version = "14.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/fe/75/af448d8e52bf1d8fa6a9d089ca6c07ff4453d86c65c145d0a300bb073b9b/rich-14.1.0.tar.gz", hash = "sha256:e497a48b844b0320d45007cdebfeaeed8db2a4f4bcf49f15e455cfc4af11eaa8", size = 224441, upload-time = "2025-07-25T07:32:58.125Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/30/3c4d035596d3cf444529e0b2953ad0466f6049528a879d27534700580395/rich-14.1.0-py3-none-any.whl", hash = "sha256:536f5f1785986d6dbdea3c75205c473f970777b4a0d6c6dd1b696aa05a3fa04f", size = 243368, upload-time = "2025-07-25T07:32:56.73Z" }, +] + +[[package]] +name = "rpds-py" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8c/a6/60184b7fc00dd3ca80ac635dd5b8577d444c57e8e8742cecabfacb829921/rpds_py-0.25.1.tar.gz", hash = "sha256:8960b6dac09b62dac26e75d7e2c4a22efb835d827a7278c34f72b2b84fa160e3", size = 27304, upload-time = "2025-05-21T12:46:12.502Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7f/81/28ab0408391b1dc57393653b6a0cf2014cc282cc2909e4615e63e58262be/rpds_py-0.25.1-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:b5ffe453cde61f73fea9430223c81d29e2fbf412a6073951102146c84e19e34c", size = 364647, upload-time = "2025-05-21T12:43:28.559Z" }, + { url = "https://files.pythonhosted.org/packages/2c/9a/7797f04cad0d5e56310e1238434f71fc6939d0bc517192a18bb99a72a95f/rpds_py-0.25.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:115874ae5e2fdcfc16b2aedc95b5eef4aebe91b28e7e21951eda8a5dc0d3461b", size = 350454, upload-time = "2025-05-21T12:43:30.615Z" }, + { url = "https://files.pythonhosted.org/packages/69/3c/93d2ef941b04898011e5d6eaa56a1acf46a3b4c9f4b3ad1bbcbafa0bee1f/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a714bf6e5e81b0e570d01f56e0c89c6375101b8463999ead3a93a5d2a4af91fa", size = 389665, upload-time = "2025-05-21T12:43:32.629Z" }, + { url = "https://files.pythonhosted.org/packages/c1/57/ad0e31e928751dde8903a11102559628d24173428a0f85e25e187defb2c1/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:35634369325906bcd01577da4c19e3b9541a15e99f31e91a02d010816b49bfda", size = 403873, upload-time = "2025-05-21T12:43:34.576Z" }, + { url = "https://files.pythonhosted.org/packages/16/ad/c0c652fa9bba778b4f54980a02962748479dc09632e1fd34e5282cf2556c/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d4cb2b3ddc16710548801c6fcc0cfcdeeff9dafbc983f77265877793f2660309", size = 525866, upload-time = "2025-05-21T12:43:36.123Z" }, + { url = "https://files.pythonhosted.org/packages/2a/39/3e1839bc527e6fcf48d5fec4770070f872cdee6c6fbc9b259932f4e88a38/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9ceca1cf097ed77e1a51f1dbc8d174d10cb5931c188a4505ff9f3e119dfe519b", size = 416886, upload-time = "2025-05-21T12:43:38.034Z" }, + { url = "https://files.pythonhosted.org/packages/7a/95/dd6b91cd4560da41df9d7030a038298a67d24f8ca38e150562644c829c48/rpds_py-0.25.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:2c2cd1a4b0c2b8c5e31ffff50d09f39906fe351389ba143c195566056c13a7ea", size = 390666, upload-time = "2025-05-21T12:43:40.065Z" }, + { url = "https://files.pythonhosted.org/packages/64/48/1be88a820e7494ce0a15c2d390ccb7c52212370badabf128e6a7bb4cb802/rpds_py-0.25.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1de336a4b164c9188cb23f3703adb74a7623ab32d20090d0e9bf499a2203ad65", size = 425109, upload-time = "2025-05-21T12:43:42.263Z" }, + { url = "https://files.pythonhosted.org/packages/cf/07/3e2a17927ef6d7720b9949ec1b37d1e963b829ad0387f7af18d923d5cfa5/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9fca84a15333e925dd59ce01da0ffe2ffe0d6e5d29a9eeba2148916d1824948c", size = 567244, upload-time = "2025-05-21T12:43:43.846Z" }, + { url = "https://files.pythonhosted.org/packages/d2/e5/76cf010998deccc4f95305d827847e2eae9c568099c06b405cf96384762b/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:88ec04afe0c59fa64e2f6ea0dd9657e04fc83e38de90f6de201954b4d4eb59bd", size = 596023, upload-time = "2025-05-21T12:43:45.932Z" }, + { url = "https://files.pythonhosted.org/packages/52/9a/df55efd84403736ba37a5a6377b70aad0fd1cb469a9109ee8a1e21299a1c/rpds_py-0.25.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a8bd2f19e312ce3e1d2c635618e8a8d8132892bb746a7cf74780a489f0f6cdcb", size = 561634, upload-time = "2025-05-21T12:43:48.263Z" }, + { url = "https://files.pythonhosted.org/packages/ab/aa/dc3620dd8db84454aaf9374bd318f1aa02578bba5e567f5bf6b79492aca4/rpds_py-0.25.1-cp312-cp312-win32.whl", hash = "sha256:e5e2f7280d8d0d3ef06f3ec1b4fd598d386cc6f0721e54f09109a8132182fbfe", size = 222713, upload-time = "2025-05-21T12:43:49.897Z" }, + { url = "https://files.pythonhosted.org/packages/a3/7f/7cef485269a50ed5b4e9bae145f512d2a111ca638ae70cc101f661b4defd/rpds_py-0.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:db58483f71c5db67d643857404da360dce3573031586034b7d59f245144cc192", size = 235280, upload-time = "2025-05-21T12:43:51.893Z" }, + { url = "https://files.pythonhosted.org/packages/99/f2/c2d64f6564f32af913bf5f3f7ae41c7c263c5ae4c4e8f1a17af8af66cd46/rpds_py-0.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:6d50841c425d16faf3206ddbba44c21aa3310a0cebc3c1cdfc3e3f4f9f6f5728", size = 225399, upload-time = "2025-05-21T12:43:53.351Z" }, + { url = "https://files.pythonhosted.org/packages/2b/da/323848a2b62abe6a0fec16ebe199dc6889c5d0a332458da8985b2980dffe/rpds_py-0.25.1-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:659d87430a8c8c704d52d094f5ba6fa72ef13b4d385b7e542a08fc240cb4a559", size = 364498, upload-time = "2025-05-21T12:43:54.841Z" }, + { url = "https://files.pythonhosted.org/packages/1f/b4/4d3820f731c80fd0cd823b3e95b9963fec681ae45ba35b5281a42382c67d/rpds_py-0.25.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:68f6f060f0bbdfb0245267da014d3a6da9be127fe3e8cc4a68c6f833f8a23bb1", size = 350083, upload-time = "2025-05-21T12:43:56.428Z" }, + { url = "https://files.pythonhosted.org/packages/d5/b1/3a8ee1c9d480e8493619a437dec685d005f706b69253286f50f498cbdbcf/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:083a9513a33e0b92cf6e7a6366036c6bb43ea595332c1ab5c8ae329e4bcc0a9c", size = 389023, upload-time = "2025-05-21T12:43:57.995Z" }, + { url = "https://files.pythonhosted.org/packages/3b/31/17293edcfc934dc62c3bf74a0cb449ecd549531f956b72287203e6880b87/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:816568614ecb22b18a010c7a12559c19f6fe993526af88e95a76d5a60b8b75fb", size = 403283, upload-time = "2025-05-21T12:43:59.546Z" }, + { url = "https://files.pythonhosted.org/packages/d1/ca/e0f0bc1a75a8925024f343258c8ecbd8828f8997ea2ac71e02f67b6f5299/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3c6564c0947a7f52e4792983f8e6cf9bac140438ebf81f527a21d944f2fd0a40", size = 524634, upload-time = "2025-05-21T12:44:01.087Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/5d0be919037178fff33a6672ffc0afa04ea1cfcb61afd4119d1b5280ff0f/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5c4a128527fe415d73cf1f70a9a688d06130d5810be69f3b553bf7b45e8acf79", size = 416233, upload-time = "2025-05-21T12:44:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/05/7c/8abb70f9017a231c6c961a8941403ed6557664c0913e1bf413cbdc039e75/rpds_py-0.25.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a49e1d7a4978ed554f095430b89ecc23f42014a50ac385eb0c4d163ce213c325", size = 390375, upload-time = "2025-05-21T12:44:04.162Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ac/a87f339f0e066b9535074a9f403b9313fd3892d4a164d5d5f5875ac9f29f/rpds_py-0.25.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d74ec9bc0e2feb81d3f16946b005748119c0f52a153f6db6a29e8cd68636f295", size = 424537, upload-time = "2025-05-21T12:44:06.175Z" }, + { url = "https://files.pythonhosted.org/packages/1f/8f/8d5c1567eaf8c8afe98a838dd24de5013ce6e8f53a01bd47fe8bb06b5533/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:3af5b4cc10fa41e5bc64e5c198a1b2d2864337f8fcbb9a67e747e34002ce812b", size = 566425, upload-time = "2025-05-21T12:44:08.242Z" }, + { url = "https://files.pythonhosted.org/packages/95/33/03016a6be5663b389c8ab0bbbcca68d9e96af14faeff0a04affcb587e776/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:79dc317a5f1c51fd9c6a0c4f48209c6b8526d0524a6904fc1076476e79b00f98", size = 595197, upload-time = "2025-05-21T12:44:10.449Z" }, + { url = "https://files.pythonhosted.org/packages/33/8d/da9f4d3e208c82fda311bff0cf0a19579afceb77cf456e46c559a1c075ba/rpds_py-0.25.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:1521031351865e0181bc585147624d66b3b00a84109b57fcb7a779c3ec3772cd", size = 561244, upload-time = "2025-05-21T12:44:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b3/39d5dcf7c5f742ecd6dbc88f6f84ae54184b92f5f387a4053be2107b17f1/rpds_py-0.25.1-cp313-cp313-win32.whl", hash = "sha256:5d473be2b13600b93a5675d78f59e63b51b1ba2d0476893415dfbb5477e65b31", size = 222254, upload-time = "2025-05-21T12:44:14.261Z" }, + { url = "https://files.pythonhosted.org/packages/5f/19/2d6772c8eeb8302c5f834e6d0dfd83935a884e7c5ce16340c7eaf89ce925/rpds_py-0.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:a7b74e92a3b212390bdce1d93da9f6488c3878c1d434c5e751cbc202c5e09500", size = 234741, upload-time = "2025-05-21T12:44:16.236Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/145ada26cfaf86018d0eb304fe55eafdd4f0b6b84530246bb4a7c4fb5c4b/rpds_py-0.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:dd326a81afe332ede08eb39ab75b301d5676802cdffd3a8f287a5f0b694dc3f5", size = 224830, upload-time = "2025-05-21T12:44:17.749Z" }, + { url = "https://files.pythonhosted.org/packages/4b/ca/d435844829c384fd2c22754ff65889c5c556a675d2ed9eb0e148435c6690/rpds_py-0.25.1-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:a58d1ed49a94d4183483a3ce0af22f20318d4a1434acee255d683ad90bf78129", size = 359668, upload-time = "2025-05-21T12:44:19.322Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/b056f21db3a09f89410d493d2f6614d87bb162499f98b649d1dbd2a81988/rpds_py-0.25.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:f251bf23deb8332823aef1da169d5d89fa84c89f67bdfb566c49dea1fccfd50d", size = 345649, upload-time = "2025-05-21T12:44:20.962Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0f/e0d00dc991e3d40e03ca36383b44995126c36b3eafa0ccbbd19664709c88/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8dbd586bfa270c1103ece2109314dd423df1fa3d9719928b5d09e4840cec0d72", size = 384776, upload-time = "2025-05-21T12:44:22.516Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a2/59374837f105f2ca79bde3c3cd1065b2f8c01678900924949f6392eab66d/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6d273f136e912aa101a9274c3145dcbddbe4bac560e77e6d5b3c9f6e0ed06d34", size = 395131, upload-time = "2025-05-21T12:44:24.147Z" }, + { url = "https://files.pythonhosted.org/packages/9c/dc/48e8d84887627a0fe0bac53f0b4631e90976fd5d35fff8be66b8e4f3916b/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:666fa7b1bd0a3810a7f18f6d3a25ccd8866291fbbc3c9b912b917a6715874bb9", size = 520942, upload-time = "2025-05-21T12:44:25.915Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f5/ee056966aeae401913d37befeeab57a4a43a4f00099e0a20297f17b8f00c/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:921954d7fbf3fccc7de8f717799304b14b6d9a45bbeec5a8d7408ccbf531faf5", size = 411330, upload-time = "2025-05-21T12:44:27.638Z" }, + { url = "https://files.pythonhosted.org/packages/ab/74/b2cffb46a097cefe5d17f94ede7a174184b9d158a0aeb195f39f2c0361e8/rpds_py-0.25.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f3d86373ff19ca0441ebeb696ef64cb58b8b5cbacffcda5a0ec2f3911732a194", size = 387339, upload-time = "2025-05-21T12:44:29.292Z" }, + { url = "https://files.pythonhosted.org/packages/7f/9a/0ff0b375dcb5161c2b7054e7d0b7575f1680127505945f5cabaac890bc07/rpds_py-0.25.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c8980cde3bb8575e7c956a530f2c217c1d6aac453474bf3ea0f9c89868b531b6", size = 418077, upload-time = "2025-05-21T12:44:30.877Z" }, + { url = "https://files.pythonhosted.org/packages/0d/a1/fda629bf20d6b698ae84c7c840cfb0e9e4200f664fc96e1f456f00e4ad6e/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8eb8c84ecea987a2523e057c0d950bcb3f789696c0499290b8d7b3107a719d78", size = 562441, upload-time = "2025-05-21T12:44:32.541Z" }, + { url = "https://files.pythonhosted.org/packages/20/15/ce4b5257f654132f326f4acd87268e1006cc071e2c59794c5bdf4bebbb51/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:e43a005671a9ed5a650f3bc39e4dbccd6d4326b24fb5ea8be5f3a43a6f576c72", size = 590750, upload-time = "2025-05-21T12:44:34.557Z" }, + { url = "https://files.pythonhosted.org/packages/fb/ab/e04bf58a8d375aeedb5268edcc835c6a660ebf79d4384d8e0889439448b0/rpds_py-0.25.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:58f77c60956501a4a627749a6dcb78dac522f249dd96b5c9f1c6af29bfacfb66", size = 558891, upload-time = "2025-05-21T12:44:37.358Z" }, + { url = "https://files.pythonhosted.org/packages/90/82/cb8c6028a6ef6cd2b7991e2e4ced01c854b6236ecf51e81b64b569c43d73/rpds_py-0.25.1-cp313-cp313t-win32.whl", hash = "sha256:2cb9e5b5e26fc02c8a4345048cd9998c2aca7c2712bd1b36da0c72ee969a3523", size = 218718, upload-time = "2025-05-21T12:44:38.969Z" }, + { url = "https://files.pythonhosted.org/packages/b6/97/5a4b59697111c89477d20ba8a44df9ca16b41e737fa569d5ae8bff99e650/rpds_py-0.25.1-cp313-cp313t-win_amd64.whl", hash = "sha256:401ca1c4a20cc0510d3435d89c069fe0a9ae2ee6495135ac46bdd49ec0495763", size = 232218, upload-time = "2025-05-21T12:44:40.512Z" }, +] + +[[package]] +name = "ruff" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/02/df/8d7d8c515d33adfc540e2edf6c6021ea1c5a58a678d8cfce9fae59aabcab/ruff-0.13.2.tar.gz", hash = "sha256:cb12fffd32fb16d32cef4ed16d8c7cdc27ed7c944eaa98d99d01ab7ab0b710ff", size = 5416417, upload-time = "2025-09-25T14:54:09.936Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6e/84/5716a7fa4758e41bf70e603e13637c42cfb9dbf7ceb07180211b9bbf75ef/ruff-0.13.2-py3-none-linux_armv6l.whl", hash = "sha256:3796345842b55f033a78285e4f1641078f902020d8450cade03aad01bffd81c3", size = 12343254, upload-time = "2025-09-25T14:53:27.784Z" }, + { url = "https://files.pythonhosted.org/packages/9b/77/c7042582401bb9ac8eff25360e9335e901d7a1c0749a2b28ba4ecb239991/ruff-0.13.2-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:ff7e4dda12e683e9709ac89e2dd436abf31a4d8a8fc3d89656231ed808e231d2", size = 13040891, upload-time = "2025-09-25T14:53:31.38Z" }, + { url = "https://files.pythonhosted.org/packages/c6/15/125a7f76eb295cb34d19c6778e3a82ace33730ad4e6f28d3427e134a02e0/ruff-0.13.2-py3-none-macosx_11_0_arm64.whl", hash = "sha256:c75e9d2a2fafd1fdd895d0e7e24b44355984affdde1c412a6f6d3f6e16b22d46", size = 12243588, upload-time = "2025-09-25T14:53:33.543Z" }, + { url = "https://files.pythonhosted.org/packages/9e/eb/0093ae04a70f81f8be7fd7ed6456e926b65d238fc122311293d033fdf91e/ruff-0.13.2-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cceac74e7bbc53ed7d15d1042ffe7b6577bf294611ad90393bf9b2a0f0ec7cb6", size = 12491359, upload-time = "2025-09-25T14:53:35.892Z" }, + { url = "https://files.pythonhosted.org/packages/43/fe/72b525948a6956f07dad4a6f122336b6a05f2e3fd27471cea612349fedb9/ruff-0.13.2-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6ae3f469b5465ba6d9721383ae9d49310c19b452a161b57507764d7ef15f4b07", size = 12162486, upload-time = "2025-09-25T14:53:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/6a/e3/0fac422bbbfb2ea838023e0d9fcf1f30183d83ab2482800e2cb892d02dfe/ruff-0.13.2-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4f8f9e3cd6714358238cd6626b9d43026ed19c0c018376ac1ef3c3a04ffb42d8", size = 13871203, upload-time = "2025-09-25T14:53:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/6b/82/b721c8e3ec5df6d83ba0e45dcf00892c4f98b325256c42c38ef136496cbf/ruff-0.13.2-py3-none-manylinux_2_17_ppc64.manylinux2014_ppc64.whl", hash = "sha256:c6ed79584a8f6cbe2e5d7dbacf7cc1ee29cbdb5df1172e77fbdadc8bb85a1f89", size = 14929635, upload-time = "2025-09-25T14:53:43.953Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a0/ad56faf6daa507b83079a1ad7a11694b87d61e6bf01c66bd82b466f21821/ruff-0.13.2-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:aed130b2fde049cea2019f55deb939103123cdd191105f97a0599a3e753d61b0", size = 14338783, upload-time = "2025-09-25T14:53:46.205Z" }, + { url = "https://files.pythonhosted.org/packages/47/77/ad1d9156db8f99cd01ee7e29d74b34050e8075a8438e589121fcd25c4b08/ruff-0.13.2-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1887c230c2c9d65ed1b4e4cfe4d255577ea28b718ae226c348ae68df958191aa", size = 13355322, upload-time = "2025-09-25T14:53:48.164Z" }, + { url = "https://files.pythonhosted.org/packages/64/8b/e87cfca2be6f8b9f41f0bb12dc48c6455e2d66df46fe61bb441a226f1089/ruff-0.13.2-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5bcb10276b69b3cfea3a102ca119ffe5c6ba3901e20e60cf9efb53fa417633c3", size = 13354427, upload-time = "2025-09-25T14:53:50.486Z" }, + { url = "https://files.pythonhosted.org/packages/7f/df/bf382f3fbead082a575edb860897287f42b1b3c694bafa16bc9904c11ed3/ruff-0.13.2-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:afa721017aa55a555b2ff7944816587f1cb813c2c0a882d158f59b832da1660d", size = 13537637, upload-time = "2025-09-25T14:53:52.887Z" }, + { url = "https://files.pythonhosted.org/packages/51/70/1fb7a7c8a6fc8bd15636288a46e209e81913b87988f26e1913d0851e54f4/ruff-0.13.2-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:1dbc875cf3720c64b3990fef8939334e74cb0ca65b8dbc61d1f439201a38101b", size = 12340025, upload-time = "2025-09-25T14:53:54.88Z" }, + { url = "https://files.pythonhosted.org/packages/4c/27/1e5b3f1c23ca5dd4106d9d580e5c13d9acb70288bff614b3d7b638378cc9/ruff-0.13.2-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:5b939a1b2a960e9742e9a347e5bbc9b3c3d2c716f86c6ae273d9cbd64f193f22", size = 12133449, upload-time = "2025-09-25T14:53:57.089Z" }, + { url = "https://files.pythonhosted.org/packages/2d/09/b92a5ccee289f11ab128df57d5911224197d8d55ef3bd2043534ff72ca54/ruff-0.13.2-py3-none-musllinux_1_2_i686.whl", hash = "sha256:50e2d52acb8de3804fc5f6e2fa3ae9bdc6812410a9e46837e673ad1f90a18736", size = 13051369, upload-time = "2025-09-25T14:53:59.124Z" }, + { url = "https://files.pythonhosted.org/packages/89/99/26c9d1c7d8150f45e346dc045cc49f23e961efceb4a70c47dea0960dea9a/ruff-0.13.2-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:3196bc13ab2110c176b9a4ae5ff7ab676faaa1964b330a1383ba20e1e19645f2", size = 13523644, upload-time = "2025-09-25T14:54:01.622Z" }, + { url = "https://files.pythonhosted.org/packages/f7/00/e7f1501e81e8ec290e79527827af1d88f541d8d26151751b46108978dade/ruff-0.13.2-py3-none-win32.whl", hash = "sha256:7c2a0b7c1e87795fec3404a485096bcd790216c7c146a922d121d8b9c8f1aaac", size = 12245990, upload-time = "2025-09-25T14:54:03.647Z" }, + { url = "https://files.pythonhosted.org/packages/ee/bd/d9f33a73de84fafd0146c6fba4f497c4565fe8fa8b46874b8e438869abc2/ruff-0.13.2-py3-none-win_amd64.whl", hash = "sha256:17d95fb32218357c89355f6f6f9a804133e404fc1f65694372e02a557edf8585", size = 13324004, upload-time = "2025-09-25T14:54:06.05Z" }, + { url = "https://files.pythonhosted.org/packages/c3/12/28fa2f597a605884deb0f65c1b1ae05111051b2a7030f5d8a4ff7f4599ba/ruff-0.13.2-py3-none-win_arm64.whl", hash = "sha256:da711b14c530412c827219312b7d7fbb4877fb31150083add7e8c5336549cea7", size = 12484437, upload-time = "2025-09-25T14:54:08.022Z" }, +] + +[[package]] +name = "safehttpx" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/67/4c/19db75e6405692b2a96af8f06d1258f8aa7290bdc35ac966f03e207f6d7f/safehttpx-0.1.6.tar.gz", hash = "sha256:b356bfc82cee3a24c395b94a2dbeabbed60aff1aa5fa3b5fe97c4f2456ebce42", size = 9987, upload-time = "2024-12-02T18:44:10.226Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/c0/1108ad9f01567f66b3154063605b350b69c3c9366732e09e45f9fd0d1deb/safehttpx-0.1.6-py3-none-any.whl", hash = "sha256:407cff0b410b071623087c63dd2080c3b44dc076888d8c5823c00d1e58cb381c", size = 8692, upload-time = "2024-12-02T18:44:08.555Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/98/c2/a7855e41c9d285dfe86dc50b250978105dce513d6e459ea66a6aeb0e1e0c/scikit_learn-1.7.2.tar.gz", hash = "sha256:20e9e49ecd130598f1ca38a1d85090e1a600147b9c02fa6f15d69cb53d968fda", size = 7193136, upload-time = "2025-09-09T08:21:29.075Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/aa/3996e2196075689afb9fce0410ebdb4a09099d7964d061d7213700204409/scikit_learn-1.7.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:8d91a97fa2b706943822398ab943cde71858a50245e31bc71dba62aab1d60a96", size = 9259818, upload-time = "2025-09-09T08:20:43.19Z" }, + { url = "https://files.pythonhosted.org/packages/43/5d/779320063e88af9c4a7c2cf463ff11c21ac9c8bd730c4a294b0000b666c9/scikit_learn-1.7.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:acbc0f5fd2edd3432a22c69bed78e837c70cf896cd7993d71d51ba6708507476", size = 8636997, upload-time = "2025-09-09T08:20:45.468Z" }, + { url = "https://files.pythonhosted.org/packages/5c/d0/0c577d9325b05594fdd33aa970bf53fb673f051a45496842caee13cfd7fe/scikit_learn-1.7.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e5bf3d930aee75a65478df91ac1225ff89cd28e9ac7bd1196853a9229b6adb0b", size = 9478381, upload-time = "2025-09-09T08:20:47.982Z" }, + { url = "https://files.pythonhosted.org/packages/82/70/8bf44b933837ba8494ca0fc9a9ab60f1c13b062ad0197f60a56e2fc4c43e/scikit_learn-1.7.2-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b4d6e9deed1a47aca9fe2f267ab8e8fe82ee20b4526b2c0cd9e135cea10feb44", size = 9300296, upload-time = "2025-09-09T08:20:50.366Z" }, + { url = "https://files.pythonhosted.org/packages/c6/99/ed35197a158f1fdc2fe7c3680e9c70d0128f662e1fee4ed495f4b5e13db0/scikit_learn-1.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:6088aa475f0785e01bcf8529f55280a3d7d298679f50c0bb70a2364a82d0b290", size = 8731256, upload-time = "2025-09-09T08:20:52.627Z" }, + { url = "https://files.pythonhosted.org/packages/ae/93/a3038cb0293037fd335f77f31fe053b89c72f17b1c8908c576c29d953e84/scikit_learn-1.7.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0b7dacaa05e5d76759fb071558a8b5130f4845166d88654a0f9bdf3eb57851b7", size = 9212382, upload-time = "2025-09-09T08:20:54.731Z" }, + { url = "https://files.pythonhosted.org/packages/40/dd/9a88879b0c1104259136146e4742026b52df8540c39fec21a6383f8292c7/scikit_learn-1.7.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:abebbd61ad9e1deed54cca45caea8ad5f79e1b93173dece40bb8e0c658dbe6fe", size = 8592042, upload-time = "2025-09-09T08:20:57.313Z" }, + { url = "https://files.pythonhosted.org/packages/46/af/c5e286471b7d10871b811b72ae794ac5fe2989c0a2df07f0ec723030f5f5/scikit_learn-1.7.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:502c18e39849c0ea1a5d681af1dbcf15f6cce601aebb657aabbfe84133c1907f", size = 9434180, upload-time = "2025-09-09T08:20:59.671Z" }, + { url = "https://files.pythonhosted.org/packages/f1/fd/df59faa53312d585023b2da27e866524ffb8faf87a68516c23896c718320/scikit_learn-1.7.2-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a4c328a71785382fe3fe676a9ecf2c86189249beff90bf85e22bdb7efaf9ae0", size = 9283660, upload-time = "2025-09-09T08:21:01.71Z" }, + { url = "https://files.pythonhosted.org/packages/a7/c7/03000262759d7b6f38c836ff9d512f438a70d8a8ddae68ee80de72dcfb63/scikit_learn-1.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:63a9afd6f7b229aad94618c01c252ce9e6fa97918c5ca19c9a17a087d819440c", size = 8702057, upload-time = "2025-09-09T08:21:04.234Z" }, + { url = "https://files.pythonhosted.org/packages/55/87/ef5eb1f267084532c8e4aef98a28b6ffe7425acbfd64b5e2f2e066bc29b3/scikit_learn-1.7.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:9acb6c5e867447b4e1390930e3944a005e2cb115922e693c08a323421a6966e8", size = 9558731, upload-time = "2025-09-09T08:21:06.381Z" }, + { url = "https://files.pythonhosted.org/packages/93/f8/6c1e3fc14b10118068d7938878a9f3f4e6d7b74a8ddb1e5bed65159ccda8/scikit_learn-1.7.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:2a41e2a0ef45063e654152ec9d8bcfc39f7afce35b08902bfe290c2498a67a6a", size = 9038852, upload-time = "2025-09-09T08:21:08.628Z" }, + { url = "https://files.pythonhosted.org/packages/83/87/066cafc896ee540c34becf95d30375fe5cbe93c3b75a0ee9aa852cd60021/scikit_learn-1.7.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:98335fb98509b73385b3ab2bd0639b1f610541d3988ee675c670371d6a87aa7c", size = 9527094, upload-time = "2025-09-09T08:21:11.486Z" }, + { url = "https://files.pythonhosted.org/packages/9c/2b/4903e1ccafa1f6453b1ab78413938c8800633988c838aa0be386cbb33072/scikit_learn-1.7.2-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:191e5550980d45449126e23ed1d5e9e24b2c68329ee1f691a3987476e115e09c", size = 9367436, upload-time = "2025-09-09T08:21:13.602Z" }, + { url = "https://files.pythonhosted.org/packages/b5/aa/8444be3cfb10451617ff9d177b3c190288f4563e6c50ff02728be67ad094/scikit_learn-1.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:57dc4deb1d3762c75d685507fbd0bc17160144b2f2ba4ccea5dc285ab0d0e973", size = 9275749, upload-time = "2025-09-09T08:21:15.96Z" }, + { url = "https://files.pythonhosted.org/packages/d9/82/dee5acf66837852e8e68df6d8d3a6cb22d3df997b733b032f513d95205b7/scikit_learn-1.7.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fa8f63940e29c82d1e67a45d5297bdebbcb585f5a5a50c4914cc2e852ab77f33", size = 9208906, upload-time = "2025-09-09T08:21:18.557Z" }, + { url = "https://files.pythonhosted.org/packages/3c/30/9029e54e17b87cb7d50d51a5926429c683d5b4c1732f0507a6c3bed9bf65/scikit_learn-1.7.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:f95dc55b7902b91331fa4e5845dd5bde0580c9cd9612b1b2791b7e80c3d32615", size = 8627836, upload-time = "2025-09-09T08:21:20.695Z" }, + { url = "https://files.pythonhosted.org/packages/60/18/4a52c635c71b536879f4b971c2cedf32c35ee78f48367885ed8025d1f7ee/scikit_learn-1.7.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9656e4a53e54578ad10a434dc1f993330568cfee176dff07112b8785fb413106", size = 9426236, upload-time = "2025-09-09T08:21:22.645Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/290362f6ab582128c53445458a5befd471ed1ea37953d5bcf80604619250/scikit_learn-1.7.2-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96dc05a854add0e50d3f47a1ef21a10a595016da5b007c7d9cd9d0bffd1fcc61", size = 9312593, upload-time = "2025-09-09T08:21:24.65Z" }, + { url = "https://files.pythonhosted.org/packages/8e/87/24f541b6d62b1794939ae6422f8023703bbf6900378b2b34e0b4384dfefd/scikit_learn-1.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:bb24510ed3f9f61476181e4db51ce801e2ba37541def12dc9333b946fc7a9cf8", size = 8820007, upload-time = "2025-09-09T08:21:26.713Z" }, +] + +[[package]] +name = "scipy" +version = "1.16.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4c/3b/546a6f0bfe791bbb7f8d591613454d15097e53f906308ec6f7c1ce588e8e/scipy-1.16.2.tar.gz", hash = "sha256:af029b153d243a80afb6eabe40b0a07f8e35c9adc269c019f364ad747f826a6b", size = 30580599, upload-time = "2025-09-11T17:48:08.271Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/8d/6396e00db1282279a4ddd507c5f5e11f606812b608ee58517ce8abbf883f/scipy-1.16.2-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:89d6c100fa5c48472047632e06f0876b3c4931aac1f4291afc81a3644316bb0d", size = 36646259, upload-time = "2025-09-11T17:40:39.329Z" }, + { url = "https://files.pythonhosted.org/packages/3b/93/ea9edd7e193fceb8eef149804491890bde73fb169c896b61aa3e2d1e4e77/scipy-1.16.2-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:ca748936cd579d3f01928b30a17dc474550b01272d8046e3e1ee593f23620371", size = 28888976, upload-time = "2025-09-11T17:40:46.82Z" }, + { url = "https://files.pythonhosted.org/packages/91/4d/281fddc3d80fd738ba86fd3aed9202331180b01e2c78eaae0642f22f7e83/scipy-1.16.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:fac4f8ce2ddb40e2e3d0f7ec36d2a1e7f92559a2471e59aec37bd8d9de01fec0", size = 20879905, upload-time = "2025-09-11T17:40:52.545Z" }, + { url = "https://files.pythonhosted.org/packages/69/40/b33b74c84606fd301b2915f0062e45733c6ff5708d121dd0deaa8871e2d0/scipy-1.16.2-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:033570f1dcefd79547a88e18bccacff025c8c647a330381064f561d43b821232", size = 23553066, upload-time = "2025-09-11T17:40:59.014Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/22c739e2f21a42cc8f16bc76b47cff4ed54fbe0962832c589591c2abec34/scipy-1.16.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ea3421209bf00c8a5ef2227de496601087d8f638a2363ee09af059bd70976dc1", size = 33336407, upload-time = "2025-09-11T17:41:06.796Z" }, + { url = "https://files.pythonhosted.org/packages/53/11/a0160990b82999b45874dc60c0c183d3a3a969a563fffc476d5a9995c407/scipy-1.16.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f66bd07ba6f84cd4a380b41d1bf3c59ea488b590a2ff96744845163309ee8e2f", size = 35673281, upload-time = "2025-09-11T17:41:15.055Z" }, + { url = "https://files.pythonhosted.org/packages/96/53/7ef48a4cfcf243c3d0f1643f5887c81f29fdf76911c4e49331828e19fc0a/scipy-1.16.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e9feab931bd2aea4a23388c962df6468af3d808ddf2d40f94a81c5dc38f32ef", size = 36004222, upload-time = "2025-09-11T17:41:23.868Z" }, + { url = "https://files.pythonhosted.org/packages/49/7f/71a69e0afd460049d41c65c630c919c537815277dfea214031005f474d78/scipy-1.16.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:03dfc75e52f72cf23ec2ced468645321407faad8f0fe7b1f5b49264adbc29cb1", size = 38664586, upload-time = "2025-09-11T17:41:31.021Z" }, + { url = "https://files.pythonhosted.org/packages/34/95/20e02ca66fb495a95fba0642fd48e0c390d0ece9b9b14c6e931a60a12dea/scipy-1.16.2-cp312-cp312-win_amd64.whl", hash = "sha256:0ce54e07bbb394b417457409a64fd015be623f36e330ac49306433ffe04bc97e", size = 38550641, upload-time = "2025-09-11T17:41:36.61Z" }, + { url = "https://files.pythonhosted.org/packages/92/ad/13646b9beb0a95528ca46d52b7babafbe115017814a611f2065ee4e61d20/scipy-1.16.2-cp312-cp312-win_arm64.whl", hash = "sha256:2a8ffaa4ac0df81a0b94577b18ee079f13fecdb924df3328fc44a7dc5ac46851", size = 25456070, upload-time = "2025-09-11T17:41:41.3Z" }, + { url = "https://files.pythonhosted.org/packages/c1/27/c5b52f1ee81727a9fc457f5ac1e9bf3d6eab311805ea615c83c27ba06400/scipy-1.16.2-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:84f7bf944b43e20b8a894f5fe593976926744f6c185bacfcbdfbb62736b5cc70", size = 36604856, upload-time = "2025-09-11T17:41:47.695Z" }, + { url = "https://files.pythonhosted.org/packages/32/a9/15c20d08e950b540184caa8ced675ba1128accb0e09c653780ba023a4110/scipy-1.16.2-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:5c39026d12edc826a1ef2ad35ad1e6d7f087f934bb868fc43fa3049c8b8508f9", size = 28864626, upload-time = "2025-09-11T17:41:52.642Z" }, + { url = "https://files.pythonhosted.org/packages/4c/fc/ea36098df653cca26062a627c1a94b0de659e97127c8491e18713ca0e3b9/scipy-1.16.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e52729ffd45b68777c5319560014d6fd251294200625d9d70fd8626516fc49f5", size = 20855689, upload-time = "2025-09-11T17:41:57.886Z" }, + { url = "https://files.pythonhosted.org/packages/dc/6f/d0b53be55727f3e6d7c72687ec18ea6d0047cf95f1f77488b99a2bafaee1/scipy-1.16.2-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:024dd4a118cccec09ca3209b7e8e614931a6ffb804b2a601839499cb88bdf925", size = 23512151, upload-time = "2025-09-11T17:42:02.303Z" }, + { url = "https://files.pythonhosted.org/packages/11/85/bf7dab56e5c4b1d3d8eef92ca8ede788418ad38a7dc3ff50262f00808760/scipy-1.16.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7a5dc7ee9c33019973a470556081b0fd3c9f4c44019191039f9769183141a4d9", size = 33329824, upload-time = "2025-09-11T17:42:07.549Z" }, + { url = "https://files.pythonhosted.org/packages/da/6a/1a927b14ddc7714111ea51f4e568203b2bb6ed59bdd036d62127c1a360c8/scipy-1.16.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:c2275ff105e508942f99d4e3bc56b6ef5e4b3c0af970386ca56b777608ce95b7", size = 35681881, upload-time = "2025-09-11T17:42:13.255Z" }, + { url = "https://files.pythonhosted.org/packages/c1/5f/331148ea5780b4fcc7007a4a6a6ee0a0c1507a796365cc642d4d226e1c3a/scipy-1.16.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:af80196eaa84f033e48444d2e0786ec47d328ba00c71e4299b602235ffef9acb", size = 36006219, upload-time = "2025-09-11T17:42:18.765Z" }, + { url = "https://files.pythonhosted.org/packages/46/3a/e991aa9d2aec723b4a8dcfbfc8365edec5d5e5f9f133888067f1cbb7dfc1/scipy-1.16.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9fb1eb735fe3d6ed1f89918224e3385fbf6f9e23757cacc35f9c78d3b712dd6e", size = 38682147, upload-time = "2025-09-11T17:42:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/a1/57/0f38e396ad19e41b4c5db66130167eef8ee620a49bc7d0512e3bb67e0cab/scipy-1.16.2-cp313-cp313-win_amd64.whl", hash = "sha256:fda714cf45ba43c9d3bae8f2585c777f64e3f89a2e073b668b32ede412d8f52c", size = 38520766, upload-time = "2025-09-11T17:43:25.342Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a5/85d3e867b6822d331e26c862a91375bb7746a0b458db5effa093d34cdb89/scipy-1.16.2-cp313-cp313-win_arm64.whl", hash = "sha256:2f5350da923ccfd0b00e07c3e5cfb316c1c0d6c1d864c07a72d092e9f20db104", size = 25451169, upload-time = "2025-09-11T17:43:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/09/d9/60679189bcebda55992d1a45498de6d080dcaf21ce0c8f24f888117e0c2d/scipy-1.16.2-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:53d8d2ee29b925344c13bda64ab51785f016b1b9617849dac10897f0701b20c1", size = 37012682, upload-time = "2025-09-11T17:42:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/83/be/a99d13ee4d3b7887a96f8c71361b9659ba4ef34da0338f14891e102a127f/scipy-1.16.2-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:9e05e33657efb4c6a9d23bd8300101536abd99c85cca82da0bffff8d8764d08a", size = 29389926, upload-time = "2025-09-11T17:42:35.845Z" }, + { url = "https://files.pythonhosted.org/packages/bf/0a/130164a4881cec6ca8c00faf3b57926f28ed429cd6001a673f83c7c2a579/scipy-1.16.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:7fe65b36036357003b3ef9d37547abeefaa353b237e989c21027b8ed62b12d4f", size = 21381152, upload-time = "2025-09-11T17:42:40.07Z" }, + { url = "https://files.pythonhosted.org/packages/47/a6/503ffb0310ae77fba874e10cddfc4a1280bdcca1d13c3751b8c3c2996cf8/scipy-1.16.2-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:6406d2ac6d40b861cccf57f49592f9779071655e9f75cd4f977fa0bdd09cb2e4", size = 23914410, upload-time = "2025-09-11T17:42:44.313Z" }, + { url = "https://files.pythonhosted.org/packages/fa/c7/1147774bcea50d00c02600aadaa919facbd8537997a62496270133536ed6/scipy-1.16.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ff4dc42bd321991fbf611c23fc35912d690f731c9914bf3af8f417e64aca0f21", size = 33481880, upload-time = "2025-09-11T17:42:49.325Z" }, + { url = "https://files.pythonhosted.org/packages/6a/74/99d5415e4c3e46b2586f30cdbecb95e101c7192628a484a40dd0d163811a/scipy-1.16.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:654324826654d4d9133e10675325708fb954bc84dae6e9ad0a52e75c6b1a01d7", size = 35791425, upload-time = "2025-09-11T17:42:54.711Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ee/a6559de7c1cc710e938c0355d9d4fbcd732dac4d0d131959d1f3b63eb29c/scipy-1.16.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:63870a84cd15c44e65220eaed2dac0e8f8b26bbb991456a033c1d9abfe8a94f8", size = 36178622, upload-time = "2025-09-11T17:43:00.375Z" }, + { url = "https://files.pythonhosted.org/packages/4e/7b/f127a5795d5ba8ece4e0dce7d4a9fb7cb9e4f4757137757d7a69ab7d4f1a/scipy-1.16.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:fa01f0f6a3050fa6a9771a95d5faccc8e2f5a92b4a2e5440a0fa7264a2398472", size = 38783985, upload-time = "2025-09-11T17:43:06.661Z" }, + { url = "https://files.pythonhosted.org/packages/3e/9f/bc81c1d1e033951eb5912cd3750cc005943afa3e65a725d2443a3b3c4347/scipy-1.16.2-cp313-cp313t-win_amd64.whl", hash = "sha256:116296e89fba96f76353a8579820c2512f6e55835d3fad7780fece04367de351", size = 38631367, upload-time = "2025-09-11T17:43:14.44Z" }, + { url = "https://files.pythonhosted.org/packages/d6/5e/2cc7555fd81d01814271412a1d59a289d25f8b63208a0a16c21069d55d3e/scipy-1.16.2-cp313-cp313t-win_arm64.whl", hash = "sha256:98e22834650be81d42982360382b43b17f7ba95e0e6993e2a4f5b9ad9283a94d", size = 25787992, upload-time = "2025-09-11T17:43:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/8b/ac/ad8951250516db71619f0bd3b2eb2448db04b720a003dd98619b78b692c0/scipy-1.16.2-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:567e77755019bb7461513c87f02bb73fb65b11f049aaaa8ca17cfaa5a5c45d77", size = 36595109, upload-time = "2025-09-11T17:43:35.713Z" }, + { url = "https://files.pythonhosted.org/packages/ff/f6/5779049ed119c5b503b0f3dc6d6f3f68eefc3a9190d4ad4c276f854f051b/scipy-1.16.2-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:17d9bb346194e8967296621208fcdfd39b55498ef7d2f376884d5ac47cec1a70", size = 28859110, upload-time = "2025-09-11T17:43:40.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/09/9986e410ae38bf0a0c737ff8189ac81a93b8e42349aac009891c054403d7/scipy-1.16.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:0a17541827a9b78b777d33b623a6dcfe2ef4a25806204d08ead0768f4e529a88", size = 20850110, upload-time = "2025-09-11T17:43:44.981Z" }, + { url = "https://files.pythonhosted.org/packages/0d/ad/485cdef2d9215e2a7df6d61b81d2ac073dfacf6ae24b9ae87274c4e936ae/scipy-1.16.2-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:d7d4c6ba016ffc0f9568d012f5f1eb77ddd99412aea121e6fa8b4c3b7cbad91f", size = 23497014, upload-time = "2025-09-11T17:43:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/a7/74/f6a852e5d581122b8f0f831f1d1e32fb8987776ed3658e95c377d308ed86/scipy-1.16.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9702c4c023227785c779cba2e1d6f7635dbb5b2e0936cdd3a4ecb98d78fd41eb", size = 33401155, upload-time = "2025-09-11T17:43:54.661Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f5/61d243bbc7c6e5e4e13dde9887e84a5cbe9e0f75fd09843044af1590844e/scipy-1.16.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d1cdf0ac28948d225decdefcc45ad7dd91716c29ab56ef32f8e0d50657dffcc7", size = 35691174, upload-time = "2025-09-11T17:44:00.101Z" }, + { url = "https://files.pythonhosted.org/packages/03/99/59933956331f8cc57e406cdb7a483906c74706b156998f322913e789c7e1/scipy-1.16.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:70327d6aa572a17c2941cdfb20673f82e536e91850a2e4cb0c5b858b690e1548", size = 36070752, upload-time = "2025-09-11T17:44:05.619Z" }, + { url = "https://files.pythonhosted.org/packages/c6/7d/00f825cfb47ee19ef74ecf01244b43e95eae74e7e0ff796026ea7cd98456/scipy-1.16.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5221c0b2a4b58aa7c4ed0387d360fd90ee9086d383bb34d9f2789fafddc8a936", size = 38701010, upload-time = "2025-09-11T17:44:11.322Z" }, + { url = "https://files.pythonhosted.org/packages/e4/9f/b62587029980378304ba5a8563d376c96f40b1e133daacee76efdcae32de/scipy-1.16.2-cp314-cp314-win_amd64.whl", hash = "sha256:f5a85d7b2b708025af08f060a496dd261055b617d776fc05a1a1cc69e09fe9ff", size = 39360061, upload-time = "2025-09-11T17:45:09.814Z" }, + { url = "https://files.pythonhosted.org/packages/82/04/7a2f1609921352c7fbee0815811b5050582f67f19983096c4769867ca45f/scipy-1.16.2-cp314-cp314-win_arm64.whl", hash = "sha256:2cc73a33305b4b24556957d5857d6253ce1e2dcd67fa0ff46d87d1670b3e1e1d", size = 26126914, upload-time = "2025-09-11T17:45:14.73Z" }, + { url = "https://files.pythonhosted.org/packages/51/b9/60929ce350c16b221928725d2d1d7f86cf96b8bc07415547057d1196dc92/scipy-1.16.2-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:9ea2a3fed83065d77367775d689401a703d0f697420719ee10c0780bcab594d8", size = 37013193, upload-time = "2025-09-11T17:44:16.757Z" }, + { url = "https://files.pythonhosted.org/packages/2a/41/ed80e67782d4bc5fc85a966bc356c601afddd175856ba7c7bb6d9490607e/scipy-1.16.2-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:7280d926f11ca945c3ef92ba960fa924e1465f8d07ce3a9923080363390624c4", size = 29390172, upload-time = "2025-09-11T17:44:21.783Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a3/2f673ace4090452696ccded5f5f8efffb353b8f3628f823a110e0170b605/scipy-1.16.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:8afae1756f6a1fe04636407ef7dbece33d826a5d462b74f3d0eb82deabefd831", size = 21381326, upload-time = "2025-09-11T17:44:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/42/bf/59df61c5d51395066c35836b78136accf506197617c8662e60ea209881e1/scipy-1.16.2-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:5c66511f29aa8d233388e7416a3f20d5cae7a2744d5cee2ecd38c081f4e861b3", size = 23915036, upload-time = "2025-09-11T17:44:30.527Z" }, + { url = "https://files.pythonhosted.org/packages/91/c3/edc7b300dc16847ad3672f1a6f3f7c5d13522b21b84b81c265f4f2760d4a/scipy-1.16.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:efe6305aeaa0e96b0ccca5ff647a43737d9a092064a3894e46c414db84bc54ac", size = 33484341, upload-time = "2025-09-11T17:44:35.981Z" }, + { url = "https://files.pythonhosted.org/packages/26/c7/24d1524e72f06ff141e8d04b833c20db3021020563272ccb1b83860082a9/scipy-1.16.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f3a337d9ae06a1e8d655ee9d8ecb835ea5ddcdcbd8d23012afa055ab014f374", size = 35790840, upload-time = "2025-09-11T17:44:41.76Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b7/5aaad984eeedd56858dc33d75efa59e8ce798d918e1033ef62d2708f2c3d/scipy-1.16.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bab3605795d269067d8ce78a910220262711b753de8913d3deeaedb5dded3bb6", size = 36174716, upload-time = "2025-09-11T17:44:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c2/e276a237acb09824822b0ada11b028ed4067fdc367a946730979feacb870/scipy-1.16.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b0348d8ddb55be2a844c518cd8cc8deeeb8aeba707cf834db5758fc89b476a2c", size = 38790088, upload-time = "2025-09-11T17:44:53.011Z" }, + { url = "https://files.pythonhosted.org/packages/c6/b4/5c18a766e8353015439f3780f5fc473f36f9762edc1a2e45da3ff5a31b21/scipy-1.16.2-cp314-cp314t-win_amd64.whl", hash = "sha256:26284797e38b8a75e14ea6631d29bda11e76ceaa6ddb6fdebbfe4c4d90faf2f9", size = 39457455, upload-time = "2025-09-11T17:44:58.899Z" }, + { url = "https://files.pythonhosted.org/packages/97/30/2f9a5243008f76dfc5dee9a53dfb939d9b31e16ce4bd4f2e628bfc5d89d2/scipy-1.16.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d2a4472c231328d4de38d5f1f68fdd6d28a615138f842580a8a321b5845cf779", size = 26448374, upload-time = "2025-09-11T17:45:03.45Z" }, +] + +[[package]] +name = "semantic-version" +version = "2.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/31/f2289ce78b9b473d582568c234e104d2a342fd658cc288a7553d83bb8595/semantic_version-2.10.0.tar.gz", hash = "sha256:bdabb6d336998cbb378d4b9db3a4b56a1e3235701dc05ea2690d9a997ed5041c", size = 52289, upload-time = "2022-05-26T13:35:23.454Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/23/8146aad7d88f4fcb3a6218f41a60f6c2d4e3a72de72da1825dc7c8f7877c/semantic_version-2.10.0-py2.py3-none-any.whl", hash = "sha256:de78a3b8e0feda74cabc54aab2da702113e33ac9d9eb9d2389bcf1f58b7d9177", size = 15552, upload-time = "2022-05-26T13:35:21.206Z" }, +] + +[[package]] +name = "send2trash" +version = "1.8.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/fd/3a/aec9b02217bb79b87bbc1a21bc6abc51e3d5dcf65c30487ac96c0908c722/Send2Trash-1.8.3.tar.gz", hash = "sha256:b18e7a3966d99871aefeb00cfbcfdced55ce4871194810fc71f4aa484b953abf", size = 17394, upload-time = "2024-04-07T00:01:09.267Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/40/b0/4562db6223154aa4e22f939003cb92514c79f3d4dccca3444253fd17f902/Send2Trash-1.8.3-py3-none-any.whl", hash = "sha256:0c31227e0bd08961c7665474a3d1ef7193929fedda4233843689baa056be46c9", size = 18072, upload-time = "2024-04-07T00:01:07.438Z" }, +] + +[[package]] +name = "setuptools" +version = "80.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/18/5d/3bf57dcd21979b887f014ea83c24ae194cfcd12b9e0fda66b957c69d1fca/setuptools-80.9.0.tar.gz", hash = "sha256:f36b47402ecde768dbfafc46e8e4207b4360c654f1f3bb84475f0a28628fb19c", size = 1319958, upload-time = "2025-05-27T00:56:51.443Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a3/dc/17031897dae0efacfea57dfd3a82fdd2a2aeb58e0ff71b77b87e44edc772/setuptools-80.9.0-py3-none-any.whl", hash = "sha256:062d34222ad13e0cc312a4c02d73f059e86a4acbfbdea8f8f76b28c99f306922", size = 1201486, upload-time = "2025-05-27T00:56:49.664Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + +[[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 = "smmap" +version = "5.0.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/44/cd/a040c4b3119bbe532e5b0732286f805445375489fceaec1f48306068ee3b/smmap-5.0.2.tar.gz", hash = "sha256:26ea65a03958fa0c8a1c7e8c7a58fdc77221b8910f6be2131affade476898ad5", size = 22329, upload-time = "2025-01-02T07:14:40.909Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/04/be/d09147ad1ec7934636ad912901c5fd7667e1c858e19d355237db0d0cd5e4/smmap-5.0.2-py3-none-any.whl", hash = "sha256:b30115f0def7d7531d22a0fb6502488d879e75b260a9db4d0819cfb25403af5e", size = 24303, upload-time = "2025-01-02T07:14:38.724Z" }, +] + +[[package]] +name = "sniffio" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372, upload-time = "2024-02-25T23:20:04.057Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, +] + +[[package]] +name = "soupsieve" +version = "2.7" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/3f/f4/4a80cd6ef364b2e8b65b15816a843c0980f7a5a2b4dc701fc574952aa19f/soupsieve-2.7.tar.gz", hash = "sha256:ad282f9b6926286d2ead4750552c8a6142bc4c783fd66b0293547c8fe6ae126a", size = 103418, upload-time = "2025-04-20T18:50:08.518Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/9c/0e6afc12c269578be5c0c1c9f4b49a8d32770a080260c333ac04cc1c832d/soupsieve-2.7-py3-none-any.whl", hash = "sha256:6e60cc5c1ffaf1cebcc12e8188320b72071e922c2e897f737cadce79ad5d30c4", size = 36677, upload-time = "2025-04-20T18:50:07.196Z" }, +] + +[[package]] +name = "stack-data" +version = "0.6.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "asttokens" }, + { name = "executing" }, + { name = "pure-eval" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/28/e3/55dcc2cfbc3ca9c29519eb6884dd1415ecb53b0e934862d3559ddcb7e20b/stack_data-0.6.3.tar.gz", hash = "sha256:836a778de4fec4dcd1dcd89ed8abff8a221f58308462e1c4aa2a3cf30148f0b9", size = 44707, upload-time = "2023-09-30T13:58:05.479Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/7b/ce1eafaf1a76852e2ec9b22edecf1daa58175c090266e9f6c64afcd81d91/stack_data-0.6.3-py3-none-any.whl", hash = "sha256:d5558e0c25a4cb0853cddad3d77da9891a08cb85dd9f9f91b9f8cd66e511e695", size = 24521, upload-time = "2023-09-30T13:58:03.53Z" }, +] + +[[package]] +name = "starlette" +version = "0.48.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "anyio" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a5/d6f429d43394057b67a6b5bbe6eae2f77a6bf7459d961fdb224bf206eee6/starlette-0.48.0.tar.gz", hash = "sha256:7e8cee469a8ab2352911528110ce9088fdc6a37d9876926e73da7ce4aa4c7a46", size = 2652949, upload-time = "2025-09-13T08:41:05.699Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/72/2db2f49247d0a18b4f1bb9a5a39a0162869acf235f3a96418363947b3d46/starlette-0.48.0-py3-none-any.whl", hash = "sha256:0764ca97b097582558ecb498132ed0c7d942f233f365b86ba37770e026510659", size = 73736, upload-time = "2025-09-13T08:41:03.869Z" }, +] + +[[package]] +name = "storage3" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "deprecation" }, + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cd/ff/1e87d4ce41490e384727122e8db3d721d58cf5a859962d999cee4246e82b/storage3-2.20.0.tar.gz", hash = "sha256:ea1d7b403ec72468b3bda6d7a4c00939070b982e7a2fcf5f3cd384bc69b4397c", size = 9415, upload-time = "2025-09-22T19:13:24.544Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d4/ba/3b204fa64d9bf88e42579369257220657a0836a40bb056b7acb7cb90b516/storage3-2.20.0-py3-none-any.whl", hash = "sha256:876cac3208c42eadcb77dbf342f9657ccc3bbe7cf32a8bac3a46b0696ab4c9df", size = 18321, upload-time = "2025-09-22T19:13:23.081Z" }, +] + +[[package]] +name = "strenum" +version = "0.4.15" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/85/ad/430fb60d90e1d112a62ff57bdd1f286ec73a2a0331272febfddd21f330e1/StrEnum-0.4.15.tar.gz", hash = "sha256:878fb5ab705442070e4dd1929bb5e2249511c0bcf2b0eeacf3bcd80875c82eff", size = 23384, upload-time = "2023-06-29T22:02:58.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/69/297302c5f5f59c862faa31e6cb9a4cd74721cd1e052b38e464c5b402df8b/StrEnum-0.4.15-py3-none-any.whl", hash = "sha256:a30cda4af7cc6b5bf52c8055bc4bf4b2b6b14a93b574626da33df53cf7740659", size = 8851, upload-time = "2023-06-29T22:02:56.947Z" }, +] + +[[package]] +name = "supabase" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx" }, + { name = "postgrest" }, + { name = "realtime" }, + { name = "storage3" }, + { name = "supabase-auth" }, + { name = "supabase-functions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/a6/cdf48dcf6a06fdc05d9e56bf664a40f2ef55f45d640374522409c980d795/supabase-2.20.0.tar.gz", hash = "sha256:6b6e740e79cee6424b32e4213dbb861cdaa21124f167ffb4aed1a0be2f48c936", size = 9346, upload-time = "2025-09-22T19:13:27.288Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/ba/f36ed9733c662abaccbcea9862d3c364fc2af803de0c61f680ba2e420480/supabase-2.20.0-py3-none-any.whl", hash = "sha256:50c07664ed1d34348a277e4d8e2596da480b6422ab0121d5e2a571cc1c2bd08a", size = 16354, upload-time = "2025-09-22T19:13:25.471Z" }, +] + +[[package]] +name = "supabase-auth" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "pydantic" }, + { name = "pyjwt", extra = ["crypto"] }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/aa/c8ee2d2ea6a46befa9254727160fcc3f2045a37c62f287bab65d32c12f67/supabase_auth-2.20.0.tar.gz", hash = "sha256:827db7722f4aee2174394f37745ef45c02db6d0da9947a5922288ceb4bb1b7d8", size = 35456, upload-time = "2025-09-22T19:13:29.847Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/80/6041d8d5bd77eee801ca70bfea3c97a2750566daf4d6f281a683bbae282a/supabase_auth-2.20.0-py3-none-any.whl", hash = "sha256:148ecebb72be00a448daca4ba3f8b5daa2b7a04ee18e385c288d48bad88924ca", size = 43964, upload-time = "2025-09-22T19:13:28.322Z" }, +] + +[[package]] +name = "supabase-functions" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "httpx", extra = ["http2"] }, + { name = "strenum" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a1/0b/8cbdb6cfa0fe0f0e43e61d63e4ac505bcd43e4c8ee5f016f289422fb7b90/supabase_functions-2.20.0.tar.gz", hash = "sha256:1fc2d5b36e12d19ef646d78cf28c515885f7f5a591372414259f5c145eb01ae3", size = 4540, upload-time = "2025-09-22T19:13:32.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/49/662e4237d8b7acfae05f578534345521f429816ee45f5b0ea00da4fbe96a/supabase_functions-2.20.0-py3-none-any.whl", hash = "sha256:f79a2523f2ff7da4d635e7534ac48116c5947157cf32163f08d369ddb302126d", size = 8520, upload-time = "2025-09-22T19:13:31.067Z" }, +] + +[[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 = "tensorboard" +version = "2.20.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "absl-py" }, + { name = "grpcio" }, + { name = "markdown" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "protobuf" }, + { name = "setuptools" }, + { name = "tensorboard-data-server" }, + { name = "werkzeug" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/9c/d9/a5db55f88f258ac669a92858b70a714bbbd5acd993820b41ec4a96a4d77f/tensorboard-2.20.0-py3-none-any.whl", hash = "sha256:9dc9f978cb84c0723acf9a345d96c184f0293d18f166bb8d59ee098e6cfaaba6", size = 5525680, upload-time = "2025-07-17T19:20:49.638Z" }, +] + +[[package]] +name = "tensorboard-data-server" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7a/13/e503968fefabd4c6b2650af21e110aa8466fe21432cd7c43a84577a89438/tensorboard_data_server-0.7.2-py3-none-any.whl", hash = "sha256:7e0610d205889588983836ec05dc098e80f97b7e7bbff7e994ebb78f578d0ddb", size = 2356, upload-time = "2023-10-23T21:23:32.16Z" }, + { url = "https://files.pythonhosted.org/packages/b7/85/dabeaf902892922777492e1d253bb7e1264cadce3cea932f7ff599e53fea/tensorboard_data_server-0.7.2-py3-none-macosx_10_9_x86_64.whl", hash = "sha256:9fe5d24221b29625dbc7328b0436ca7fc1c23de4acf4d272f1180856e32f9f60", size = 4823598, upload-time = "2023-10-23T21:23:33.714Z" }, + { url = "https://files.pythonhosted.org/packages/73/c6/825dab04195756cf8ff2e12698f22513b3db2f64925bdd41671bfb33aaa5/tensorboard_data_server-0.7.2-py3-none-manylinux_2_31_x86_64.whl", hash = "sha256:ef687163c24185ae9754ed5650eb5bc4d84ff257aabdc33f0cc6f74d8ba54530", size = 6590363, upload-time = "2023-10-23T21:23:35.583Z" }, +] + +[[package]] +name = "terminado" +version = "0.18.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "ptyprocess", marker = "os_name != 'nt'" }, + { name = "pywinpty", marker = "os_name == 'nt'" }, + { name = "tornado" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8a/11/965c6fd8e5cc254f1fe142d547387da17a8ebfd75a3455f637c663fb38a0/terminado-0.18.1.tar.gz", hash = "sha256:de09f2c4b85de4765f7714688fff57d3e75bad1f909b589fde880460c753fd2e", size = 32701, upload-time = "2024-03-12T14:34:39.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/9e/2064975477fdc887e47ad42157e214526dcad8f317a948dee17e1659a62f/terminado-0.18.1-py3-none-any.whl", hash = "sha256:a4468e1b37bb318f8a86514f65814e1afc977cf29b3992a4500d9dd305dcceb0", size = 14154, upload-time = "2024-03-12T14:34:36.569Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tinycss2" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "webencodings" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/fd/7a5ee21fd08ff70d3d33a5781c255cbe779659bd03278feb98b19ee550f4/tinycss2-1.4.0.tar.gz", hash = "sha256:10c0972f6fc0fbee87c3edb76549357415e94548c1ae10ebccdea16fb404a9b7", size = 87085, upload-time = "2024-10-24T14:58:29.895Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/34/ebdc18bae6aa14fbee1a08b63c015c72b64868ff7dae68808ab500c492e2/tinycss2-1.4.0-py3-none-any.whl", hash = "sha256:3a49cf47b7675da0b15d0c6e1df8df4ebd96e9394bb905a5775adb0d884c5289", size = 26610, upload-time = "2024-10-24T14:58:28.029Z" }, +] + +[[package]] +name = "tomlkit" +version = "0.13.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cc/18/0bbf3884e9eaa38819ebe46a7bd25dcd56b67434402b66a58c4b8e552575/tomlkit-0.13.3.tar.gz", hash = "sha256:430cf247ee57df2b94ee3fbe588e71d362a941ebb545dec29b53961d61add2a1", size = 185207, upload-time = "2025-06-05T07:13:44.947Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bd/75/8539d011f6be8e29f339c42e633aae3cb73bffa95dd0f9adec09b9c58e85/tomlkit-0.13.3-py3-none-any.whl", hash = "sha256:c89c649d79ee40629a9fda55f8ace8c6a1b42deb912b2a8fd8d942ddadb606b0", size = 38901, upload-time = "2025-06-05T07:13:43.546Z" }, +] + +[[package]] +name = "torch" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { 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-nvtx-cu12", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "platform_machine == 'x86_64' and sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/93/fb505a5022a2e908d81fe9a5e0aa84c86c0d5f408173be71c6018836f34e/torch-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:27ea1e518df4c9de73af7e8a720770f3628e7f667280bce2be7a16292697e3fa", size = 98948276, upload-time = "2025-06-04T17:39:12.852Z" }, + { url = "https://files.pythonhosted.org/packages/56/7e/67c3fe2b8c33f40af06326a3d6ae7776b3e3a01daa8f71d125d78594d874/torch-2.7.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:c33360cfc2edd976c2633b3b66c769bdcbbf0e0b6550606d188431c81e7dd1fc", size = 821025792, upload-time = "2025-06-04T17:34:58.747Z" }, + { url = "https://files.pythonhosted.org/packages/a1/37/a37495502bc7a23bf34f89584fa5a78e25bae7b8da513bc1b8f97afb7009/torch-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:d8bf6e1856ddd1807e79dc57e54d3335f2b62e6f316ed13ed3ecfe1fc1df3d8b", size = 216050349, upload-time = "2025-06-04T17:38:59.709Z" }, + { url = "https://files.pythonhosted.org/packages/3a/60/04b77281c730bb13460628e518c52721257814ac6c298acd25757f6a175c/torch-2.7.1-cp312-none-macosx_11_0_arm64.whl", hash = "sha256:787687087412c4bd68d315e39bc1223f08aae1d16a9e9771d95eabbb04ae98fb", size = 68645146, upload-time = "2025-06-04T17:38:52.97Z" }, + { url = "https://files.pythonhosted.org/packages/66/81/e48c9edb655ee8eb8c2a6026abdb6f8d2146abd1f150979ede807bb75dcb/torch-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:03563603d931e70722dce0e11999d53aa80a375a3d78e6b39b9f6805ea0a8d28", size = 98946649, upload-time = "2025-06-04T17:38:43.031Z" }, + { url = "https://files.pythonhosted.org/packages/3a/24/efe2f520d75274fc06b695c616415a1e8a1021d87a13c68ff9dce733d088/torch-2.7.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:d632f5417b6980f61404a125b999ca6ebd0b8b4bbdbb5fbbba44374ab619a412", size = 821033192, upload-time = "2025-06-04T17:38:09.146Z" }, + { url = "https://files.pythonhosted.org/packages/dd/d9/9c24d230333ff4e9b6807274f6f8d52a864210b52ec794c5def7925f4495/torch-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:23660443e13995ee93e3d844786701ea4ca69f337027b05182f5ba053ce43b38", size = 216055668, upload-time = "2025-06-04T17:38:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/95/bf/e086ee36ddcef9299f6e708d3b6c8487c1651787bb9ee2939eb2a7f74911/torch-2.7.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:0da4f4dba9f65d0d203794e619fe7ca3247a55ffdcbd17ae8fb83c8b2dc9b585", size = 68925988, upload-time = "2025-06-04T17:38:29.273Z" }, + { url = "https://files.pythonhosted.org/packages/69/6a/67090dcfe1cf9048448b31555af6efb149f7afa0a310a366adbdada32105/torch-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:e08d7e6f21a617fe38eeb46dd2213ded43f27c072e9165dc27300c9ef9570934", size = 99028857, upload-time = "2025-06-04T17:37:50.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/1c/48b988870823d1cc381f15ec4e70ed3d65e043f43f919329b0045ae83529/torch-2.7.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:30207f672328a42df4f2174b8f426f354b2baa0b7cca3a0adb3d6ab5daf00dc8", size = 821098066, upload-time = "2025-06-04T17:37:33.939Z" }, + { url = "https://files.pythonhosted.org/packages/7b/eb/10050d61c9d5140c5dc04a89ed3257ef1a6b93e49dd91b95363d757071e0/torch-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:79042feca1c634aaf6603fe6feea8c6b30dfa140a6bbc0b973e2260c7e79a22e", size = 216336310, upload-time = "2025-06-04T17:36:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/beb45cdf5c4fc3ebe282bf5eafc8dfd925ead7299b3c97491900fe5ed844/torch-2.7.1-cp313-none-macosx_11_0_arm64.whl", hash = "sha256:988b0cbc4333618a1056d2ebad9eb10089637b659eb645434d0809d8d937b946", size = 68645708, upload-time = "2025-06-04T17:34:39.852Z" }, +] + +[[package]] +name = "torchaudio" +version = "2.7.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/d1/eb8bc3b3502dddb1b789567b7b19668b1d32817266887b9f381494cfe463/torchaudio-2.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9306dcfc4586cebd7647a93fe9a448e791c4f83934da616b9433b75597a1f978", size = 1846897, upload-time = "2025-06-04T17:44:07.79Z" }, + { url = "https://files.pythonhosted.org/packages/62/7d/6c15f15d3edc5271abc808f70713644b50f0f7bfb85a09dba8b5735fbad3/torchaudio-2.7.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:d66bd76b226fdd4135c97650e1b7eb63fb7659b4ed0e3a778898e41dbba21b61", size = 1686680, upload-time = "2025-06-04T17:43:58.986Z" }, + { url = "https://files.pythonhosted.org/packages/48/65/0f46ba74cdc67ea9a8c37c8acfb5194d81639e481e85903c076bcd97188c/torchaudio-2.7.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:9cbcdaab77ad9a73711acffee58f4eebc8a0685289a938a3fa6f660af9489aee", size = 3506966, upload-time = "2025-06-04T17:44:06.537Z" }, + { url = "https://files.pythonhosted.org/packages/52/29/06f887baf22cbba85ae331b71b110b115bf11b3968f5914a50c17dde5ab7/torchaudio-2.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:9cfb8f6ace8e01e2b89de74eb893ba5ce936b88b415383605b0a4d974009dec7", size = 2484265, upload-time = "2025-06-04T17:44:00.277Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ee/6e308868b9467e1b51da9d781cb73dd5aadca7c8b6256f88ce5d18a7fb77/torchaudio-2.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e5f0599a507f4683546878ed9667e1b32d7ca3c8a957e4c15c6b302378ef4dee", size = 1847208, upload-time = "2025-06-04T17:44:01.365Z" }, + { url = "https://files.pythonhosted.org/packages/3a/f9/ca0e0960526e6deaa476d168b877480a3fbae5d44668a54de963a9800097/torchaudio-2.7.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:271f717844e5c7f9e05c8328de817bf90f46d83281c791e94f54d4edea2f5817", size = 1686311, upload-time = "2025-06-04T17:44:02.785Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ab/83f282ca5475ae34c58520a4a97b6d69438bc699d70d16432deb19791cda/torchaudio-2.7.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:1862b063d8d4e55cb4862bcbd63568545f549825a3c5605bd312224c3ebb1919", size = 3507174, upload-time = "2025-06-04T17:43:46.526Z" }, + { url = "https://files.pythonhosted.org/packages/12/91/dbd17a6eda4b0504d9b4f1f721a1654456e39f7178b8462344f942100865/torchaudio-2.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:edb4deaa6f95acd5522912ed643303d0b86d79a6f15914362f5a5d49baaf5d13", size = 2484503, upload-time = "2025-06-04T17:43:48.169Z" }, + { url = "https://files.pythonhosted.org/packages/73/5e/da52d2fa9f7cc89512b63dd8a88fb3e097a89815f440cc16159b216ec611/torchaudio-2.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:18560955b8beb2a8d39a6bfae20a442337afcefb3dfd4ee007ce82233a796799", size = 1929983, upload-time = "2025-06-04T17:43:56.659Z" }, + { url = "https://files.pythonhosted.org/packages/f7/16/9d03dc62613f276f9666eb0609164287df23986b67d20b53e78d21a3d8d8/torchaudio-2.7.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:1850475ef9101ea0b3593fe93ff6ee4e7a20598f6da6510761220b9fe56eb7fa", size = 1700436, upload-time = "2025-06-04T17:43:55.589Z" }, + { url = "https://files.pythonhosted.org/packages/83/45/57a437fe41b302fc79b4eb78fdb3e480ff42c66270e7505eedf0b000969c/torchaudio-2.7.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98257fc14dd493ba5a3258fb6d61d27cd64a48ee79537c3964c4da26b9bf295f", size = 3521631, upload-time = "2025-06-04T17:43:50.628Z" }, + { url = "https://files.pythonhosted.org/packages/91/5e/9262a7e41e47bc87eb245c4fc485eb26ff41a05886b241c003440c9e0107/torchaudio-2.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c802e0dcbf38669007327bb52f065573cc5cac106eaca987f6e1a32e6282263a", size = 2534956, upload-time = "2025-06-04T17:43:42.324Z" }, +] + +[[package]] +name = "torchvision" +version = "0.22.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "pillow" }, + { name = "torch" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/90/f4e99a5112dc221cf68a485e853cc3d9f3f1787cb950b895f3ea26d1ea98/torchvision-0.22.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:153f1790e505bd6da123e21eee6e83e2e155df05c0fe7d56347303067d8543c5", size = 1947827, upload-time = "2025-06-04T17:43:11.945Z" }, + { url = "https://files.pythonhosted.org/packages/25/f6/53e65384cdbbe732cc2106bb04f7fb908487e4fb02ae4a1613ce6904a122/torchvision-0.22.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:964414eef19459d55a10e886e2fca50677550e243586d1678f65e3f6f6bac47a", size = 2514576, upload-time = "2025-06-04T17:43:02.707Z" }, + { url = "https://files.pythonhosted.org/packages/17/8b/155f99042f9319bd7759536779b2a5b67cbd4f89c380854670850f89a2f4/torchvision-0.22.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:699c2d70d33951187f6ed910ea05720b9b4aaac1dcc1135f53162ce7d42481d3", size = 7485962, upload-time = "2025-06-04T17:42:43.606Z" }, + { url = "https://files.pythonhosted.org/packages/05/17/e45d5cd3627efdb47587a0634179a3533593436219de3f20c743672d2a79/torchvision-0.22.1-cp312-cp312-win_amd64.whl", hash = "sha256:75e0897da7a8e43d78632f66f2bdc4f6e26da8d3f021a7c0fa83746073c2597b", size = 1707992, upload-time = "2025-06-04T17:42:53.207Z" }, + { url = "https://files.pythonhosted.org/packages/7a/30/fecdd09fb973e963da68207fe9f3d03ec6f39a935516dc2a98397bf495c6/torchvision-0.22.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:9c3ae3319624c43cc8127020f46c14aa878406781f0899bb6283ae474afeafbf", size = 1947818, upload-time = "2025-06-04T17:42:51.954Z" }, + { url = "https://files.pythonhosted.org/packages/55/f4/b45f6cd92fa0acfac5e31b8e9258232f25bcdb0709a604e8b8a39d76e411/torchvision-0.22.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:4a614a6a408d2ed74208d0ea6c28a2fbb68290e9a7df206c5fef3f0b6865d307", size = 2471597, upload-time = "2025-06-04T17:42:48.838Z" }, + { url = "https://files.pythonhosted.org/packages/8d/b0/3cffd6a285b5ffee3fe4a31caff49e350c98c5963854474d1c4f7a51dea5/torchvision-0.22.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:7ee682be589bb1a002b7704f06b8ec0b89e4b9068f48e79307d2c6e937a9fdf4", size = 7485894, upload-time = "2025-06-04T17:43:01.371Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1d/0ede596fedc2080d18108149921278b59f220fbb398f29619495337b0f86/torchvision-0.22.1-cp313-cp313-win_amd64.whl", hash = "sha256:2566cafcfa47ecfdbeed04bab8cef1307c8d4ef75046f7624b9e55f384880dfe", size = 1708020, upload-time = "2025-06-04T17:43:06.085Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ca/e9a06bd61ee8e04fb4962a3fb524fe6ee4051662db07840b702a9f339b24/torchvision-0.22.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:043d9e35ed69c2e586aff6eb9e2887382e7863707115668ac9d140da58f42cba", size = 2137623, upload-time = "2025-06-04T17:43:05.028Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c8/2ebe90f18e7ffa2120f5c3eab62aa86923185f78d2d051a455ea91461608/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:27142bcc8a984227a6dcf560985e83f52b82a7d3f5fe9051af586a2ccc46ef26", size = 2476561, upload-time = "2025-06-04T17:42:59.691Z" }, + { url = "https://files.pythonhosted.org/packages/94/8b/04c6b15f8c29b39f0679589753091cec8b192ab296d4fdaf9055544c4ec9/torchvision-0.22.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:ef46e065502f7300ad6abc98554131c35dc4c837b978d91306658f1a65c00baa", size = 7658543, upload-time = "2025-06-04T17:42:46.064Z" }, + { url = "https://files.pythonhosted.org/packages/ab/c0/131628e6d42682b0502c63fd7f647b8b5ca4bd94088f6c85ca7225db8ac4/torchvision-0.22.1-cp313-cp313t-win_amd64.whl", hash = "sha256:7414eeacfb941fa21acddcd725f1617da5630ec822e498660a4b864d7d998075", size = 1629892, upload-time = "2025-06-04T17:42:57.156Z" }, +] + +[[package]] +name = "tornado" +version = "6.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/89/c72771c81d25d53fe33e3dca61c233b665b2780f21820ba6fd2c6793c12b/tornado-6.5.1.tar.gz", hash = "sha256:84ceece391e8eb9b2b95578db65e920d2a61070260594819589609ba9bc6308c", size = 509934, upload-time = "2025-05-22T18:15:38.788Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/77/89/f4532dee6843c9e0ebc4e28d4be04c67f54f60813e4bf73d595fe7567452/tornado-6.5.1-cp39-abi3-macosx_10_9_universal2.whl", hash = "sha256:d50065ba7fd11d3bd41bcad0825227cc9a95154bad83239357094c36708001f7", size = 441948, upload-time = "2025-05-22T18:15:20.862Z" }, + { url = "https://files.pythonhosted.org/packages/15/9a/557406b62cffa395d18772e0cdcf03bed2fff03b374677348eef9f6a3792/tornado-6.5.1-cp39-abi3-macosx_10_9_x86_64.whl", hash = "sha256:9e9ca370f717997cb85606d074b0e5b247282cf5e2e1611568b8821afe0342d6", size = 440112, upload-time = "2025-05-22T18:15:22.591Z" }, + { url = "https://files.pythonhosted.org/packages/55/82/7721b7319013a3cf881f4dffa4f60ceff07b31b394e459984e7a36dc99ec/tornado-6.5.1-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b77e9dfa7ed69754a54c89d82ef746398be82f749df69c4d3abe75c4d1ff4888", size = 443672, upload-time = "2025-05-22T18:15:24.027Z" }, + { url = "https://files.pythonhosted.org/packages/7d/42/d11c4376e7d101171b94e03cef0cbce43e823ed6567ceda571f54cf6e3ce/tornado-6.5.1-cp39-abi3-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:253b76040ee3bab8bcf7ba9feb136436a3787208717a1fb9f2c16b744fba7331", size = 443019, upload-time = "2025-05-22T18:15:25.735Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f7/0c48ba992d875521ac761e6e04b0a1750f8150ae42ea26df1852d6a98942/tornado-6.5.1-cp39-abi3-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:308473f4cc5a76227157cdf904de33ac268af770b2c5f05ca6c1161d82fdd95e", size = 443252, upload-time = "2025-05-22T18:15:27.499Z" }, + { url = "https://files.pythonhosted.org/packages/89/46/d8d7413d11987e316df4ad42e16023cd62666a3c0dfa1518ffa30b8df06c/tornado-6.5.1-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:caec6314ce8a81cf69bd89909f4b633b9f523834dc1a352021775d45e51d9401", size = 443930, upload-time = "2025-05-22T18:15:29.299Z" }, + { url = "https://files.pythonhosted.org/packages/78/b2/f8049221c96a06df89bed68260e8ca94beca5ea532ffc63b1175ad31f9cc/tornado-6.5.1-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:13ce6e3396c24e2808774741331638ee6c2f50b114b97a55c5b442df65fd9692", size = 443351, upload-time = "2025-05-22T18:15:31.038Z" }, + { url = "https://files.pythonhosted.org/packages/76/ff/6a0079e65b326cc222a54720a748e04a4db246870c4da54ece4577bfa702/tornado-6.5.1-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5cae6145f4cdf5ab24744526cc0f55a17d76f02c98f4cff9daa08ae9a217448a", size = 443328, upload-time = "2025-05-22T18:15:32.426Z" }, + { url = "https://files.pythonhosted.org/packages/49/18/e3f902a1d21f14035b5bc6246a8c0f51e0eef562ace3a2cea403c1fb7021/tornado-6.5.1-cp39-abi3-win32.whl", hash = "sha256:e0a36e1bc684dca10b1aa75a31df8bdfed656831489bc1e6a6ebed05dc1ec365", size = 444396, upload-time = "2025-05-22T18:15:34.205Z" }, + { url = "https://files.pythonhosted.org/packages/7b/09/6526e32bf1049ee7de3bebba81572673b19a2a8541f795d887e92af1a8bc/tornado-6.5.1-cp39-abi3-win_amd64.whl", hash = "sha256:908e7d64567cecd4c2b458075589a775063453aeb1d2a1853eedb806922f568b", size = 444840, upload-time = "2025-05-22T18:15:36.1Z" }, + { url = "https://files.pythonhosted.org/packages/55/a7/535c44c7bea4578e48281d83c615219f3ab19e6abc67625ef637c73987be/tornado-6.5.1-cp39-abi3-win_arm64.whl", hash = "sha256:02420a0eb7bf617257b9935e2b754d1b63897525d8a289c9d65690d580b4dcf7", size = 443596, upload-time = "2025-05-22T18:15:37.433Z" }, +] + +[[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 = "traitlets" +version = "5.14.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/79/72064e6a701c2183016abbbfedaba506d81e30e232a68c9f0d6f6fcd1574/traitlets-5.14.3.tar.gz", hash = "sha256:9ed0579d3502c94b4b3732ac120375cda96f923114522847de4b3bb98b96b6b7", size = 161621, upload-time = "2024-04-19T11:11:49.746Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/c0/8f5d070730d7836adc9c9b6408dec68c6ced86b304a9b26a14df072a6e8c/traitlets-5.14.3-py3-none-any.whl", hash = "sha256:b74e89e397b1ed28cc831db7aea759ba6640cb3de13090ca145426688ff1ac4f", size = 85359, upload-time = "2024-04-19T11:11:46.763Z" }, +] + +[[package]] +name = "triton" +version = "3.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "setuptools" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/5f/950fb373bf9c01ad4eb5a8cd5eaf32cdf9e238c02f9293557a2129b9c4ac/triton-3.3.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9999e83aba21e1a78c1f36f21bce621b77bcaa530277a50484a7cb4a822f6e43", size = 155669138, upload-time = "2025-05-29T23:39:51.771Z" }, + { url = "https://files.pythonhosted.org/packages/74/1f/dfb531f90a2d367d914adfee771babbd3f1a5b26c3f5fbc458dee21daa78/triton-3.3.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b89d846b5a4198317fec27a5d3a609ea96b6d557ff44b56c23176546023c4240", size = 155673035, upload-time = "2025-05-29T23:40:02.468Z" }, + { url = "https://files.pythonhosted.org/packages/28/71/bd20ffcb7a64c753dc2463489a61bf69d531f308e390ad06390268c4ea04/triton-3.3.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3198adb9d78b77818a5388bff89fa72ff36f9da0bc689db2f0a651a67ce6a42", size = 155735832, upload-time = "2025-05-29T23:40:10.522Z" }, +] + +[[package]] +name = "typer" +version = "0.19.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/21/ca/950278884e2ca20547ff3eb109478c6baf6b8cf219318e6bc4f666fad8e8/typer-0.19.2.tar.gz", hash = "sha256:9ad824308ded0ad06cc716434705f691d4ee0bfd0fb081839d2e426860e7fdca", size = 104755, upload-time = "2025-09-23T09:47:48.256Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/00/22/35617eee79080a5d071d0f14ad698d325ee6b3bf824fc0467c03b30e7fa8/typer-0.19.2-py3-none-any.whl", hash = "sha256:755e7e19670ffad8283db353267cb81ef252f595aa6834a0d1ca9312d9326cb9", size = 46748, upload-time = "2025-09-23T09:47:46.777Z" }, +] + +[[package]] +name = "types-python-dateutil" +version = "2.9.0.20250516" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ef/88/d65ed807393285204ab6e2801e5d11fbbea811adcaa979a2ed3b67a5ef41/types_python_dateutil-2.9.0.20250516.tar.gz", hash = "sha256:13e80d6c9c47df23ad773d54b2826bd52dbbb41be87c3f339381c1700ad21ee5", size = 13943, upload-time = "2025-05-16T03:06:58.385Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c5/3f/b0e8db149896005adc938a1e7f371d6d7e9eca4053a29b108978ed15e0c2/types_python_dateutil-2.9.0.20250516-py3-none-any.whl", hash = "sha256:2b2b3f57f9c6a61fba26a9c0ffb9ea5681c9b83e69cd897c6b5f668d9c0cab93", size = 14356, upload-time = "2025-05-16T03:06:57.249Z" }, +] + +[[package]] +name = "typing-extensions" +version = "4.14.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d1/bc/51647cd02527e87d05cb083ccc402f93e441606ff1f01739a62c8ad09ba5/typing_extensions-4.14.0.tar.gz", hash = "sha256:8676b788e32f02ab42d9e7c61324048ae4c6d844a399eebace3d4979d75ceef4", size = 107423, upload-time = "2025-06-02T14:52:11.399Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/e0/552843e0d356fbb5256d21449fa957fa4eff3bbc135a74a691ee70c7c5da/typing_extensions-4.14.0-py3-none-any.whl", hash = "sha256:a1514509136dd0b477638fc68d6a91497af5076466ad0fa6c338e44e359944af", size = 43839, upload-time = "2025-06-02T14:52:10.026Z" }, +] + +[[package]] +name = "typing-inspection" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f8/b1/0c11f5058406b3af7609f121aaa6b609744687f1d158b3c3a5bf4cc94238/typing_inspection-0.4.1.tar.gz", hash = "sha256:6ae134cc0203c33377d43188d4064e9b357dba58cff3185f22924610e70a9d28", size = 75726, upload-time = "2025-05-21T18:55:23.885Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/69/cd203477f944c353c31bade965f880aa1061fd6bf05ded0726ca845b6ff7/typing_inspection-0.4.1-py3-none-any.whl", hash = "sha256:389055682238f53b04f7badcb49b989835495a96700ced5dab2d8feae4b26f51", size = 14552, upload-time = "2025-05-21T18:55:22.152Z" }, +] + +[[package]] +name = "tzdata" +version = "2025.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/95/32/1a225d6164441be760d75c2c42e2780dc0873fe382da3e98a2e1e48361e5/tzdata-2025.2.tar.gz", hash = "sha256:b60a638fcc0daffadf82fe0f57e53d06bdec2f36c4df66280ae79bce6bd6f2b9", size = 196380, upload-time = "2025-03-23T13:54:43.652Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5c/23/c7abc0ca0a1526a0774eca151daeb8de62ec457e77262b66b359c3c7679e/tzdata-2025.2-py2.py3-none-any.whl", hash = "sha256:1a403fada01ff9221ca8044d701868fa132215d84beb92242d9acd2147f667a8", size = 347839, upload-time = "2025-03-23T13:54:41.845Z" }, +] + +[[package]] +name = "uri-template" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/31/c7/0336f2bd0bcbada6ccef7aaa25e443c118a704f828a0620c6fa0207c1b64/uri-template-1.3.0.tar.gz", hash = "sha256:0e00f8eb65e18c7de20d595a14336e9f337ead580c70934141624b6d1ffdacc7", size = 21678, upload-time = "2023-06-21T01:49:05.374Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/00/3fca040d7cf8a32776d3d81a00c8ee7457e00f80c649f1e4a863c8321ae9/uri_template-1.3.0-py3-none-any.whl", hash = "sha256:a44a133ea12d44a0c0f06d7d42a52d71282e77e2f937d8abd5655b8d56fc1363", size = 11140, upload-time = "2023-06-21T01:49:03.467Z" }, +] + +[[package]] +name = "urllib3" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/8a/78/16493d9c386d8e60e442a35feac5e00f0913c0f4b7c217c11e8ec2ff53e0/urllib3-2.4.0.tar.gz", hash = "sha256:414bc6535b787febd7567804cc015fee39daab8ad86268f1310a9250697de466", size = 390672, upload-time = "2025-04-10T15:23:39.232Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6b/11/cc635220681e93a0183390e26485430ca2c7b5f9d33b15c74c2861cb8091/urllib3-2.4.0-py3-none-any.whl", hash = "sha256:4e16665048960a0900c702d4a66415956a584919c03361cac9f1df5c5dd7e813", size = 128680, upload-time = "2025-04-10T15:23:37.377Z" }, +] + +[[package]] +name = "uvicorn" +version = "0.37.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, + { name = "h11" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/71/57/1616c8274c3442d802621abf5deb230771c7a0fec9414cb6763900eb3868/uvicorn-0.37.0.tar.gz", hash = "sha256:4115c8add6d3fd536c8ee77f0e14a7fd2ebba939fed9b02583a97f80648f9e13", size = 80367, upload-time = "2025-09-23T13:33:47.486Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/85/cd/584a2ceb5532af99dd09e50919e3615ba99aa127e9850eafe5f31ddfdb9a/uvicorn-0.37.0-py3-none-any.whl", hash = "sha256:913b2b88672343739927ce381ff9e2ad62541f9f8289664fa1d1d3803fa2ce6c", size = 67976, upload-time = "2025-09-23T13:33:45.842Z" }, +] + +[[package]] +name = "wcwidth" +version = "0.2.13" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6c/63/53559446a878410fc5a5974feb13d31d78d752eb18aeba59c7fef1af7598/wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5", size = 101301, upload-time = "2024-01-06T02:10:57.829Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fd/84/fd2ba7aafacbad3c4201d395674fc6348826569da3c0937e75505ead3528/wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859", size = 34166, upload-time = "2024-01-06T02:10:55.763Z" }, +] + +[[package]] +name = "webcolors" +version = "24.11.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7b/29/061ec845fb58521848f3739e466efd8250b4b7b98c1b6c5bf4d40b419b7e/webcolors-24.11.1.tar.gz", hash = "sha256:ecb3d768f32202af770477b8b65f318fa4f566c22948673a977b00d589dd80f6", size = 45064, upload-time = "2024-11-11T07:43:24.224Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/60/e8/c0e05e4684d13459f93d312077a9a2efbe04d59c393bc2b8802248c908d4/webcolors-24.11.1-py3-none-any.whl", hash = "sha256:515291393b4cdf0eb19c155749a096f779f7d909f7cceea072791cb9095b92e9", size = 14934, upload-time = "2024-11-11T07:43:22.529Z" }, +] + +[[package]] +name = "webencodings" +version = "0.5.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0b/02/ae6ceac1baeda530866a85075641cec12989bd8d31af6d5ab4a3e8c92f47/webencodings-0.5.1.tar.gz", hash = "sha256:b36a1c245f2d304965eb4e0a82848379241dc04b865afcc4aab16748587e1923", size = 9721, upload-time = "2017-04-05T20:21:34.189Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/24/2a3e3df732393fed8b3ebf2ec078f05546de641fe1b667ee316ec1dcf3b7/webencodings-0.5.1-py2.py3-none-any.whl", hash = "sha256:a0af1213f3c2226497a97e2b3aa01a7e4bee4f403f95be16fc9acd2947514a78", size = 11774, upload-time = "2017-04-05T20:21:32.581Z" }, +] + +[[package]] +name = "websocket-client" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e6/30/fba0d96b4b5fbf5948ed3f4681f7da2f9f64512e1d303f94b4cc174c24a5/websocket_client-1.8.0.tar.gz", hash = "sha256:3239df9f44da632f96012472805d40a23281a991027ce11d2f45a6f24ac4c3da", size = 54648, upload-time = "2024-04-23T22:16:16.976Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/5a/84/44687a29792a70e111c5c477230a72c4b957d88d16141199bf9acb7537a3/websocket_client-1.8.0-py3-none-any.whl", hash = "sha256:17b44cc997f5c498e809b22cdf2d9c7a9e71c02c8cc2b6c56e7c2d1239bfa526", size = 58826, upload-time = "2024-04-23T22:16:14.422Z" }, +] + +[[package]] +name = "websockets" +version = "15.0.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/21/e6/26d09fab466b7ca9c7737474c52be4f76a40301b08362eb2dbc19dcc16c1/websockets-15.0.1.tar.gz", hash = "sha256:82544de02076bafba038ce055ee6412d68da13ab47f0c60cab827346de828dee", size = 177016, upload-time = "2025-03-05T20:03:41.606Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/51/6b/4545a0d843594f5d0771e86463606a3988b5a09ca5123136f8a76580dd63/websockets-15.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:3e90baa811a5d73f3ca0bcbf32064d663ed81318ab225ee4f427ad4e26e5aff3", size = 175437, upload-time = "2025-03-05T20:02:16.706Z" }, + { url = "https://files.pythonhosted.org/packages/f4/71/809a0f5f6a06522af902e0f2ea2757f71ead94610010cf570ab5c98e99ed/websockets-15.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:592f1a9fe869c778694f0aa806ba0374e97648ab57936f092fd9d87f8bc03665", size = 173096, upload-time = "2025-03-05T20:02:18.832Z" }, + { url = "https://files.pythonhosted.org/packages/3d/69/1a681dd6f02180916f116894181eab8b2e25b31e484c5d0eae637ec01f7c/websockets-15.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0701bc3cfcb9164d04a14b149fd74be7347a530ad3bbf15ab2c678a2cd3dd9a2", size = 173332, upload-time = "2025-03-05T20:02:20.187Z" }, + { url = "https://files.pythonhosted.org/packages/a6/02/0073b3952f5bce97eafbb35757f8d0d54812b6174ed8dd952aa08429bcc3/websockets-15.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e8b56bdcdb4505c8078cb6c7157d9811a85790f2f2b3632c7d1462ab5783d215", size = 183152, upload-time = "2025-03-05T20:02:22.286Z" }, + { url = "https://files.pythonhosted.org/packages/74/45/c205c8480eafd114b428284840da0b1be9ffd0e4f87338dc95dc6ff961a1/websockets-15.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0af68c55afbd5f07986df82831c7bff04846928ea8d1fd7f30052638788bc9b5", size = 182096, upload-time = "2025-03-05T20:02:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/14/8f/aa61f528fba38578ec553c145857a181384c72b98156f858ca5c8e82d9d3/websockets-15.0.1-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:64dee438fed052b52e4f98f76c5790513235efaa1ef7f3f2192c392cd7c91b65", size = 182523, upload-time = "2025-03-05T20:02:25.669Z" }, + { url = "https://files.pythonhosted.org/packages/ec/6d/0267396610add5bc0d0d3e77f546d4cd287200804fe02323797de77dbce9/websockets-15.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d5f6b181bb38171a8ad1d6aa58a67a6aa9d4b38d0f8c5f496b9e42561dfc62fe", size = 182790, upload-time = "2025-03-05T20:02:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/c68c5adbf679cf610ae2f74a9b871ae84564462955d991178f95a1ddb7dd/websockets-15.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:5d54b09eba2bada6011aea5375542a157637b91029687eb4fdb2dab11059c1b4", size = 182165, upload-time = "2025-03-05T20:02:30.291Z" }, + { url = "https://files.pythonhosted.org/packages/29/93/bb672df7b2f5faac89761cb5fa34f5cec45a4026c383a4b5761c6cea5c16/websockets-15.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3be571a8b5afed347da347bfcf27ba12b069d9d7f42cb8c7028b5e98bbb12597", size = 182160, upload-time = "2025-03-05T20:02:31.634Z" }, + { url = "https://files.pythonhosted.org/packages/ff/83/de1f7709376dc3ca9b7eeb4b9a07b4526b14876b6d372a4dc62312bebee0/websockets-15.0.1-cp312-cp312-win32.whl", hash = "sha256:c338ffa0520bdb12fbc527265235639fb76e7bc7faafbb93f6ba80d9c06578a9", size = 176395, upload-time = "2025-03-05T20:02:33.017Z" }, + { url = "https://files.pythonhosted.org/packages/7d/71/abf2ebc3bbfa40f391ce1428c7168fb20582d0ff57019b69ea20fa698043/websockets-15.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcd5cf9e305d7b8338754470cf69cf81f420459dbae8a3b40cee57417f4614a7", size = 176841, upload-time = "2025-03-05T20:02:34.498Z" }, + { url = "https://files.pythonhosted.org/packages/cb/9f/51f0cf64471a9d2b4d0fc6c534f323b664e7095640c34562f5182e5a7195/websockets-15.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ee443ef070bb3b6ed74514f5efaa37a252af57c90eb33b956d35c8e9c10a1931", size = 175440, upload-time = "2025-03-05T20:02:36.695Z" }, + { url = "https://files.pythonhosted.org/packages/8a/05/aa116ec9943c718905997412c5989f7ed671bc0188ee2ba89520e8765d7b/websockets-15.0.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:5a939de6b7b4e18ca683218320fc67ea886038265fd1ed30173f5ce3f8e85675", size = 173098, upload-time = "2025-03-05T20:02:37.985Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0b/33cef55ff24f2d92924923c99926dcce78e7bd922d649467f0eda8368923/websockets-15.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:746ee8dba912cd6fc889a8147168991d50ed70447bf18bcda7039f7d2e3d9151", size = 173329, upload-time = "2025-03-05T20:02:39.298Z" }, + { url = "https://files.pythonhosted.org/packages/31/1d/063b25dcc01faa8fada1469bdf769de3768b7044eac9d41f734fd7b6ad6d/websockets-15.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:595b6c3969023ecf9041b2936ac3827e4623bfa3ccf007575f04c5a6aa318c22", size = 183111, upload-time = "2025-03-05T20:02:40.595Z" }, + { url = "https://files.pythonhosted.org/packages/93/53/9a87ee494a51bf63e4ec9241c1ccc4f7c2f45fff85d5bde2ff74fcb68b9e/websockets-15.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3c714d2fc58b5ca3e285461a4cc0c9a66bd0e24c5da9911e30158286c9b5be7f", size = 182054, upload-time = "2025-03-05T20:02:41.926Z" }, + { url = "https://files.pythonhosted.org/packages/ff/b2/83a6ddf56cdcbad4e3d841fcc55d6ba7d19aeb89c50f24dd7e859ec0805f/websockets-15.0.1-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0f3c1e2ab208db911594ae5b4f79addeb3501604a165019dd221c0bdcabe4db8", size = 182496, upload-time = "2025-03-05T20:02:43.304Z" }, + { url = "https://files.pythonhosted.org/packages/98/41/e7038944ed0abf34c45aa4635ba28136f06052e08fc2168520bb8b25149f/websockets-15.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:229cf1d3ca6c1804400b0a9790dc66528e08a6a1feec0d5040e8b9eb14422375", size = 182829, upload-time = "2025-03-05T20:02:48.812Z" }, + { url = "https://files.pythonhosted.org/packages/e0/17/de15b6158680c7623c6ef0db361da965ab25d813ae54fcfeae2e5b9ef910/websockets-15.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:756c56e867a90fb00177d530dca4b097dd753cde348448a1012ed6c5131f8b7d", size = 182217, upload-time = "2025-03-05T20:02:50.14Z" }, + { url = "https://files.pythonhosted.org/packages/33/2b/1f168cb6041853eef0362fb9554c3824367c5560cbdaad89ac40f8c2edfc/websockets-15.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:558d023b3df0bffe50a04e710bc87742de35060580a293c2a984299ed83bc4e4", size = 182195, upload-time = "2025-03-05T20:02:51.561Z" }, + { url = "https://files.pythonhosted.org/packages/86/eb/20b6cdf273913d0ad05a6a14aed4b9a85591c18a987a3d47f20fa13dcc47/websockets-15.0.1-cp313-cp313-win32.whl", hash = "sha256:ba9e56e8ceeeedb2e080147ba85ffcd5cd0711b89576b83784d8605a7df455fa", size = 176393, upload-time = "2025-03-05T20:02:53.814Z" }, + { url = "https://files.pythonhosted.org/packages/1b/6c/c65773d6cab416a64d191d6ee8a8b1c68a09970ea6909d16965d26bfed1e/websockets-15.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:e09473f095a819042ecb2ab9465aee615bd9c2028e4ef7d933600a8401c79561", size = 176837, upload-time = "2025-03-05T20:02:55.237Z" }, + { url = "https://files.pythonhosted.org/packages/fa/a8/5b41e0da817d64113292ab1f8247140aac61cbf6cfd085d6a0fa77f4984f/websockets-15.0.1-py3-none-any.whl", hash = "sha256:f7a866fbc1e97b5c617ee4116daaa09b722101d4a3c170c787450ba409f9736f", size = 169743, upload-time = "2025-03-05T20:03:39.41Z" }, +] + +[[package]] +name = "werkzeug" +version = "3.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9f/69/83029f1f6300c5fb2471d621ab06f6ec6b3324685a2ce0f9777fd4a8b71e/werkzeug-3.1.3.tar.gz", hash = "sha256:60723ce945c19328679790e3282cc758aa4a6040e4bb330f53d30fa546d44746", size = 806925, upload-time = "2024-11-08T15:52:18.093Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/24/ab44c871b0f07f491e5d2ad12c9bd7358e527510618cb1b803a88e986db1/werkzeug-3.1.3-py3-none-any.whl", hash = "sha256:54b78bf3716d19a65be4fceccc0d1d7b89e608834989dfae50ea87564639213e", size = 224498, upload-time = "2024-11-08T15:52:16.132Z" }, +] + +[[package]] +name = "widgetsnbextension" +version = "4.0.14" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/53/2e0253c5efd69c9656b1843892052a31c36d37ad42812b5da45c62191f7e/widgetsnbextension-4.0.14.tar.gz", hash = "sha256:a3629b04e3edb893212df862038c7232f62973373869db5084aed739b437b5af", size = 1097428, upload-time = "2025-04-10T13:01:25.628Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/51/5447876806d1088a0f8f71e16542bf350918128d0a69437df26047c8e46f/widgetsnbextension-4.0.14-py3-none-any.whl", hash = "sha256:4875a9eaf72fbf5079dc372a51a9f268fc38d46f767cbf85c43a36da5cb9b575", size = 2196503, upload-time = "2025-04-10T13:01:23.086Z" }, +] + +[[package]] +name = "worddetectornn" +version = "0.1.0" +source = { virtual = "." } +dependencies = [ + { name = "dotenv" }, + { name = "gitpython" }, + { name = "gradio" }, + { name = "jupyter" }, + { name = "opencv-python" }, + { name = "scikit-learn" }, + { name = "supabase" }, + { name = "tensorboard" }, + { name = "torch" }, + { name = "torchaudio" }, + { name = "torchvision" }, + { name = "tqdm" }, +] + +[package.metadata] +requires-dist = [ + { name = "dotenv", specifier = ">=0.9.9" }, + { name = "gitpython", specifier = ">=3.1.45" }, + { name = "gradio", specifier = ">=5.47.2" }, + { name = "jupyter", specifier = ">=1.1.1" }, + { name = "opencv-python", specifier = ">=4.12.0.88" }, + { name = "scikit-learn", specifier = ">=1.7.2" }, + { name = "supabase", specifier = ">=2.20.0" }, + { name = "tensorboard", specifier = ">=2.20.0" }, + { name = "torch", specifier = ">=2.7.1" }, + { name = "torchaudio", specifier = ">=2.7.1" }, + { name = "torchvision", specifier = ">=0.22.1" }, + { name = "tqdm", specifier = ">=4.67.1" }, +] diff --git a/xournalpp_htr/training/__init__.py b/xournalpp_htr/training/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/xournalpp_htr/training/annotate.py b/xournalpp_htr/training/annotate.py new file mode 100644 index 0000000000000000000000000000000000000000..c3333e2f18135689c46da3b7249f574a60d9c114 --- /dev/null +++ b/xournalpp_htr/training/annotate.py @@ -0,0 +1,340 @@ +"""This script helps to annotate Xournal++ files.""" + +import argparse +import dataclasses +import datetime +import tkinter as tk +from pathlib import Path +from tkinter.filedialog import askopenfilename, asksaveasfilename + +import git +import numpy as np + +from xournalpp_htr.documents import Stroke, XournalppDocument +from xournalpp_htr.training.data import BBox +from xournalpp_htr.training.io import store_list_of_bboxes + +# ===== +# TODOs +# ===== + +# TODO: Adhere to design document called `annotate_tool_UI_design.svg`. Potentially model it in PenPot? +# TODO: Useful structure for app: https://stackoverflow.com/questions/17125842/changing-the-text-on-a-label +# TODO: Improve GUI layout; Properly read about TKINTER interface design: +# - https://tkinterpython.top/layout/ +# TODO: Everything in here is untested. I will probably leave it as is for now. + +# =========== +# Helper code +# =========== + + +def load_document(): + global currently_loaded_document + global I_PAGE + I_PAGE = int(page_selector_text.get("1.0")) + filename = askopenfilename() + currently_loaded_document = filename + status_file.configure(text=f"File loaded: {currently_loaded_document}") + status_file.update() + return filename + + +def draw_a_point(c: tk.Canvas, coord_x: float, coord_y: float, color: str) -> None: + x1, y1 = (coord_x - 1), (coord_y - 1) + x2, y2 = (coord_x + 1), (coord_y + 1) + c.create_oval(x1, y1, x2, y2, fill=color) + + +def draw_document(): + canvas.delete("all") + + xpp_document = XournalppDocument(Path(currently_loaded_document)) + + color = "black" # python_green = "#476042" # draw different colour for each stroke + + # Adjust canvas size + coord_boundaries = xpp_document.get_min_max_coordinates_per_page() + new_width = coord_boundaries[I_PAGE]["max_x"] * 1.1 + new_height = coord_boundaries[I_PAGE]["max_y"] * 1.1 + canvas.config(width=new_width, height=new_height) + + # Plot points + for layer in xpp_document.pages[I_PAGE].layers: + for stroke in layer.strokes: + for coord_x, coord_y in zip(stroke.x, stroke.y): + draw_a_point(canvas, coord_x, coord_y, color) + + if DRAW_STROKE_BOUNDING_BOX.get(): + x0 = coord_boundaries[I_PAGE]["min_x"] + y0 = coord_boundaries[I_PAGE]["max_y"] + x1 = coord_boundaries[I_PAGE]["max_x"] + y1 = coord_boundaries[I_PAGE]["min_y"] + canvas.create_rectangle(x0, y0, x1, y1, fill="", outline="red") + canvas.create_text(x1, y1, text="data bounding box", anchor=tk.SE, fill="red") + + +def draw_bbox(): + global START_DRAWING_BBOX + START_DRAWING_BBOX = True + + +def paint_bbox(event): + global START_DRAWING_BBOX + global BBOX_FIRST_POINT + global LIST_OF_BBOXES + if START_DRAWING_BBOX: + BBOX_FIRST_POINT = event.x, event.y + START_DRAWING_BBOX = False + else: + if BBOX_FIRST_POINT: + # Get point + second_point = event.x, event.y + + # Store bbox + bbox = BBox( + text=None, + point_1_x=BBOX_FIRST_POINT[0], + point_1_y=BBOX_FIRST_POINT[1], + point_2_x=second_point[0], + point_2_y=second_point[1], + capture_date=datetime.datetime.now(), + uuid=BBox.get_new_uuid(), + rect_reference=None, + strokes=None, + ) + + print(bbox) + print(dataclasses.asdict(bbox)) + + LIST_OF_BBOXES.append(bbox) + + # Draw + rect = canvas.create_rectangle( + bbox.point_1_x, + bbox.point_1_y, + bbox.point_2_x, + bbox.point_2_y, + fill="", + outline=DEFAULT_BBOX_OUTLINE_COLOR, + ) + bbox.rect_reference = rect + print( + rect, type(rect) + ) # Use it like shown here: https://stackoverflow.com/a/35935638 & https://stackoverflow.com/a/13212501 + + # Add to listview + listbox.insert(tk.END, bbox) + + # Book keeping + BBOX_FIRST_POINT = None + + +def listbox_select(event): + index = listbox.curselection()[0] + bbox = listbox.get(index, None) + for bbox in LIST_OF_BBOXES: # Reset colors + canvas.itemconfig(bbox.rect_reference, outline=DEFAULT_BBOX_OUTLINE_COLOR) + bbox: BBox = LIST_OF_BBOXES[index] + canvas.itemconfig(bbox.rect_reference, outline=HIGHLIGHTED_BBOX_OUTLINE_COLOR) + edit_text.delete(1.0, tk.END) + edit_text.insert(tk.END, "" if bbox.text is None else bbox.text) + + +def update_bbox_text(): + index = listbox.curselection()[0] + bbox = LIST_OF_BBOXES[index] + bbox.text = edit_text.get("1.0", tk.END).strip() + + +def export(): + if DEBUG: + output_path = Path( + "/home/martin/Development/xournalpp_htr/tests/data/2024-10-13_minimal.annotations.json" + ) + else: + output_path = Path( + asksaveasfilename( + initialfile="Untitled.json", + defaultextension=".json", + filetypes=[("All Files", "*.*"), ("JSON Documents", "*.json")], + ) + ) + + xpp_document = XournalppDocument(Path(currently_loaded_document)) + + # Get all strokes on that page in a list + all_strokes: list[Stroke] = [] + for layer in xpp_document.pages[I_PAGE].layers: + for stroke in layer.strokes: + all_strokes.append(stroke) + + stroke_already_used = np.zeros(len(all_strokes), dtype=bool) + + # Determine strokes + for bbox in LIST_OF_BBOXES: + min_x = min(bbox.point_1_x, bbox.point_2_x) + max_x = max(bbox.point_1_x, bbox.point_2_x) + min_y = min(bbox.point_1_y, bbox.point_2_y) + max_y = max(bbox.point_1_y, bbox.point_2_y) + bbox_strokes = [] + for i_stroke, stroke in enumerate(all_strokes): + # Skip strokes that are already part of a bbox + if stroke_already_used[i_stroke]: + continue + condition_x_min = np.all(min_x <= stroke.x) + condition_x_max = np.all(stroke.x <= max_x) + condition_y_min = np.all(min_y <= stroke.y) + condition_y_max = np.all(stroke.y <= max_y) + if ( + condition_x_min + and condition_x_max + and condition_y_min + and condition_y_max + ): + bbox_strokes.append(stroke) + stroke_already_used[i_stroke] = True + # assert bbox.strokes is None + bbox.strokes = bbox_strokes + + # TODO: Store bbox'es as JSON; I do so by storing all the + # relevant detail in a dict first. The method that turns + # it in to a JSON might as well be part of `Bbox`! + + # TODO: Also loading the JSON into a dict will be a part of + # the `Bbox` class for the sake of simplicity. + + # TODO: Add a storage schema + # TODO: define storage schema to allow backward compatibility when improving the script later on + + store_list_of_bboxes( + output_path=output_path, + list_of_bboxes=LIST_OF_BBOXES, + schema_version="v1_2024-10-13", + meta_data={ + "annotator_ID": annotator_ID.get("1.0", tk.END).strip(), + "writer_ID": writer_ID.get("1.0", tk.END).strip(), + "currently_loaded_document": str(currently_loaded_document), + "page_index": I_PAGE, + }, + ) + + +# ========= +# Main code +# ========= + + +parser = argparse.ArgumentParser(prog="annotate helper") +parser.add_argument("-d", "--debug", action="store_true", help="Enable debug mode.") +args = vars(parser.parse_args()) + +DEBUG = args["debug"] + +root = tk.Tk() # create root window +root.title("Annotate Tool") # title of the GUI window +root.geometry("1000x800") +root.config(bg="skyblue") # specify background color + + +if DEBUG: + currently_loaded_document = Path( + "/home/martin/Development/xournalpp_htr/tests/data/2024-07-26_minimal.xopp" + ) +else: + currently_loaded_document = None + + +I_PAGE = 0 + +START_DRAWING_BBOX = False + + +BBOX_FIRST_POINT = None + +LIST_OF_BBOXES: list[BBox] = [] + + +w = tk.Button(root, text="Load document", command=load_document) +w.place(x=50, y=50) + +page_selector_label = tk.Label(root, text="Select page:") +page_selector_label.place(x=200, y=50) + +page_selector_text = tk.Text( + root, + height=1, + width=4, + font=40, +) +page_selector_text.place(x=280, y=50) +page_selector_text.insert("1.0", "0") + +DRAW_STROKE_BOUNDING_BOX = tk.BooleanVar() +b = tk.Checkbutton( + root, text="Enable DRAW_STROKE_BOUNDING_BOX?", variable=DRAW_STROKE_BOUNDING_BOX +) +b.place(x=50, y=120) + +button_draw = tk.Button(root, text="Draw document", command=draw_document) +button_draw.place(x=50, y=90) + +status_file = tk.Label(root, text=f"File loaded: {currently_loaded_document}") +status_file.place(x=0, y=20) + +status_bar = tk.Label(root, text="status bar") +status_bar.place(x=0, y=0) + +canvas = tk.Canvas(root, width=500, height=500) +canvas.place(x=50, y=150) +canvas.bind("", paint_bbox) + +button_draw_bbox = tk.Button(root, text="Draw bbox (d)", command=draw_bbox) +button_draw_bbox.place(x=200, y=90) +root.bind("d", lambda event: draw_bbox()) + +DEFAULT_BBOX_OUTLINE_COLOR = "orange" +HIGHLIGHTED_BBOX_OUTLINE_COLOR = "red" + + +# create listbox object +listbox = tk.Listbox( + root, + height=10, + width=25, + bg="grey", + activestyle="dotbox", + font="Helvetica", + fg="yellow", +) +listbox.place(x=700, y=150) +# See here for what I want to do: https://tk-tutorial.readthedocs.io/en/latest/listbox/listbox.html#edit-a-listbox-item +listbox.bind("<>", listbox_select) +# Another good resource: https://www.geeksforgeeks.org/python-tkinter-listbox-widget/ + + +edit_text = tk.Text(root, height=2, width=30, font=40) +edit_text.place(x=700, y=500) + + +update_text = tk.Button(root, text="Update bbox text", command=update_bbox_text) +update_text.place(x=700, y=600) + +annotator_ID = tk.Text(root, height=2, width=30, font=40) +annotator_ID.insert(tk.END, "(add annotator ID here)") +annotator_ID.place(x=800, y=650) + +writer_ID = tk.Text(root, height=2, width=30, font=40) +writer_ID.insert(tk.END, "(add writer ID here)") +writer_ID.place(x=800, y=700) + +repo = git.Repo(search_parent_directories=True) +sha = repo.head.object.hexsha +git_commit_hash_label = tk.Label(root, text=f"git commit: {sha}") +git_commit_hash_label.place(x=500, y=0) + + +export_annotations = tk.Button(root, text="Export annotations", command=export) +export_annotations.place(x=50, y=750) + +root.mainloop() diff --git a/xournalpp_htr/training/annotation_tool/index.html b/xournalpp_htr/training/annotation_tool/index.html new file mode 100644 index 0000000000000000000000000000000000000000..f6ed1d02e765fafa9dc141f69141fe603b4ec575 --- /dev/null +++ b/xournalpp_htr/training/annotation_tool/index.html @@ -0,0 +1,20 @@ + + + + + + GZ File Uploader + + + +
+

Upload GZ File

+ + +
 
+        
+    
+ + + + diff --git a/xournalpp_htr/training/annotation_tool/script.js b/xournalpp_htr/training/annotation_tool/script.js new file mode 100644 index 0000000000000000000000000000000000000000..3fe4172e42547d856b156c501d87fe3d54c7d624 --- /dev/null +++ b/xournalpp_htr/training/annotation_tool/script.js @@ -0,0 +1,171 @@ +function readGZFile() { + const fileInput = document.getElementById('fileInput'); + const output = document.getElementById('output'); + + if (fileInput.files.length === 0) { + output.textContent = "Please select a GZ file to upload."; + return; + } + + const file = fileInput.files[0]; + const reader = new FileReader(); + + reader.onload = function(event) { + try { + const gzData = new Uint8Array(event.target.result); + const decompressedData = pako.ungzip(gzData, { to: 'string' }); + + if (isXML(decompressedData)) { + const formattedXML = formatXML(decompressedData); + // output.textContent = formattedXML; + + const strokes = getStrokesByPage(decompressedData); + // console.log(strokes); + + strokesPage = strokes[0]; // TODO: Deal w/ pages + + // ------------------ + // Adjust canvas size + // ------------------ + + // Get the canvas element and its context + const canvas = document.getElementById('myCanvas'); + + // Find max + let xMax = -1.0; + let yMax = -1.0; + for (let iStrokes = 0; iStrokes < strokesPage.length; iStrokes++) { + xValue = strokesPage[iStrokes][0]; + yValue = strokesPage[iStrokes][1]; + if (xValue > xMax) { + xMax = xValue; + } + if (yValue > yMax) { + yMax = yValue; + } + } + + // Adjust canvas + // canvas.style.width = Math.ceil(1.1*xMax).toString()+"px"; + // canvas.style.height = Math.ceil(1.1*yMax).toString()+"px"; + // TODO: This doesn't seem to work! + // canvas.style.width = "1500px"; + // canvas.style.height = "2500px"; + + // --------- + // Draw data + // --------- + + const ctx = canvas.getContext('2d'); + + // Function to plot a point on the canvas + function plotPoint(x, y, color = 'black', radius = 1) { + ctx.beginPath(); + ctx.arc(x, y, radius, 0, 2 * Math.PI); + ctx.fillStyle = color; + ctx.fill(); + ctx.closePath(); + } + + // Plot all the points + strokesPage.forEach(point => { + plotPoint(point[0], point[1], 'black'); + }); + + } else { + // output.textContent = decompressedData; + } + } catch (e) { + output.textContent = "An error occurred while reading the file: " + e.message; + } + }; + + reader.onerror = function() { + output.textContent = "Error reading file."; + }; + + reader.readAsArrayBuffer(file); + +} + + +function isXML(data) { + // Simple check to determine if the string looks like XML + return data.trim().startsWith("<") && data.trim().endsWith(">"); +} + +function formatXML(xmlString) { + try { + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, "application/xml"); + const serializer = new XMLSerializer(); + let formatted = serializer.serializeToString(xmlDoc); + + // Format the XML string with indentation + formatted = formatted.replace(/(>)(<)(\/*)/g, '$1\n$2$3'); + const lines = formatted.split('\n'); + let indent = 0; + for (let i = 0; i < lines.length; i++) { + if (lines[i].match(/.+<\/\w[^>]*>$/)) { + // No change in indent for self-closing tags + } else if (lines[i].match(/^<\/\w/) && indent > 0) { + indent--; + } + lines[i] = ' '.repeat(indent) + lines[i]; + if (lines[i].match(/^<\w([^>]*[^/])?>.*$/)) { + indent++; + } + } + return lines.join('\n'); + } catch (e) { + return "Error parsing XML: " + e.message; + } +} + +function getStrokesByPage(xmlString) { + + const parser = new DOMParser(); + const xmlDoc = parser.parseFromString(xmlString, "application/xml"); + + const pageElements = xmlDoc.getElementsByTagName('page'); + + let data = []; + + for (let iPage = 0; iPage < pageElements.length; iPage++){ + + const strokeElements = pageElements[iPage].getElementsByTagName('stroke'); + + let xCoordinates = []; + let yCoordinates = []; + + for (let i = 0; i < strokeElements.length; i++) { + + // Get the stroke value and split it by spaces into an array of coordinates + let coordinates = strokeElements[i].textContent.trim().split(/\s+/); + + // Iterate through coordinates array, separating x and y values + for (let j = 0; j < coordinates.length; j++) { + if (j % 2 === 0) { + // Even index: x coordinate + xCoordinates.push(Number(coordinates[j])); + } else { + // Odd index: y coordinate + yCoordinates.push(Number(coordinates[j])); + } + } + } + + // Add the coordinates as tuples to the data structure that stores page-wise information + let pageData = []; + + for (let k = 0; k < xCoordinates.length; k++) { + pageData.push([xCoordinates[k], yCoordinates[k]]); + } + + // Add the page data to the overall data array + data.push(pageData); + } + + // Return the data array containing all page-wise coordinates + return data; +} diff --git a/xournalpp_htr/training/annotation_tool/style.css b/xournalpp_htr/training/annotation_tool/style.css new file mode 100644 index 0000000000000000000000000000000000000000..36a70da7b4d8bff6b637bea233333a1e0a957631 --- /dev/null +++ b/xournalpp_htr/training/annotation_tool/style.css @@ -0,0 +1,40 @@ +body { + font-family: Arial, sans-serif; + display: flex; + justify-content: center; + align-items: center; + height: 100vh; + background-color: #f0f0f0; + margin: 0; +} + +.container { + background: white; + padding: 20px; + box-shadow: 0 0 10px rgba(0, 0, 0, 0.1); + border-radius: 8px; + text-align: center; +} + +h1 { + margin-bottom: 20px; +} + +input[type="file"] { + margin-bottom: 10px; +} + +pre { + text-align: left; + background: #fafafa; + padding: 15px; + border: 1px solid #ddd; + height: 300px; + overflow: auto; + white-space: pre-wrap; /* Ensures that the XML lines wrap properly */ + font-family: Consolas, 'Courier New', monospace; /* Use a monospaced font for better readability */ +} + +canvas { + border: 1px solid #000000 +} \ No newline at end of file diff --git a/xournalpp_htr/training/annotation_tool/test.html b/xournalpp_htr/training/annotation_tool/test.html new file mode 100644 index 0000000000000000000000000000000000000000..be7d7c3dee1ff142462fdfaecb85bab002713871 --- /dev/null +++ b/xournalpp_htr/training/annotation_tool/test.html @@ -0,0 +1,378 @@ + + + + CSV Data Plotter with Annotations + + + +
+

CSV Data Plotter with Annotations

+ +
+ + + + + +
+ + + +
+

Annotations:

+
    +
    +
    + +
    + + + + diff --git a/xournalpp_htr/training/data/__init__.py b/xournalpp_htr/training/data/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..a5b50e84aecb3a211e51c102b732aecf7d34b905 --- /dev/null +++ b/xournalpp_htr/training/data/__init__.py @@ -0,0 +1,36 @@ +import dataclasses +import datetime +import json +import uuid +from dataclasses import dataclass + +from xournalpp_htr.documents import Stroke + + +@dataclass +class BBox: + text: str + point_1_x: float + point_1_y: float + point_2_x: float + point_2_y: float + capture_date: datetime.datetime + uuid: str + rect_reference: int | None + strokes: list[Stroke] | None + + def __str__(self) -> str: + return str(self.capture_date) + + def to_json_str(self) -> str: + return json.dumps( + dataclasses.asdict(self), indent=4, sort_keys=True, default=str + ) + + def from_json_str(self, json_str: str) -> None: + print("from_json_str") + pass + + @staticmethod + def get_new_uuid() -> str: + return str(uuid.uuid4()) diff --git a/xournalpp_htr/training/data/datamodules.py b/xournalpp_htr/training/data/datamodules.py new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/xournalpp_htr/training/data/datasets.py b/xournalpp_htr/training/data/datasets.py new file mode 100644 index 0000000000000000000000000000000000000000..8d45b85e21cb9adb513e9e7c97cf7d7e5f340853 --- /dev/null +++ b/xournalpp_htr/training/data/datasets.py @@ -0,0 +1,716 @@ +""" +Module concerned with creating datasets for training custom +xournalpp_htr models. +""" + +import os +import xml.etree.ElementTree as ET +from collections import defaultdict +from dataclasses import dataclass +from pathlib import Path +from typing import List + +import matplotlib +import matplotlib.pyplot as plt +import numpy as np +import pandas as pd +from PIL import Image, ImageDraw +from torch.utils.data import Dataset +from torchvision.io import read_image +from tqdm import tqdm + +DatasetIndex = int +MillimeterDimension = float +DotDimension = int + + +class IAM_OnDB_Dataset(Dataset): + """IAM-OnDB dataset implementation in PyTorch. + + These are the links to the dataset: + - https://fki.tic.heia-fr.ch/databases/iam-on-line-handwriting-database + - https://doi.org/10.1109/ICDAR.2005.132 + + This class encapsulates my own version of the IAM On-DB dataset in which I fixed a few small + samples by fixing text formatting issues. + + This is the raw dataset which can be further processed using downstream transformations. + """ + + @staticmethod + def load_df_iam_ondb(path: Path) -> pd.DataFrame: + """ + Load IAM OnDB strokes file as pd.DataFrame. + + Does not read any meta data. + + :param path: The path to the XML strokes file. + :returns: pd.DataFrame with strokes stored in columns "x", "y", "t" and "stroke_nr". + """ + tree = ET.parse(path) + root = tree.getroot() + + for element in root: + if element.tag == "StrokeSet": + stroke_set = element + break + + data = {"x": [], "y": [], "t": [], "stroke_nr": []} + + stroke_nr = 0 + for stroke in stroke_set: + for point in stroke: + data["x"].append(float(point.attrib["x"])) + data["y"].append(float(point.attrib["y"])) + data["t"].append(float(point.attrib["time"])) + data["stroke_nr"].append(stroke_nr) + stroke_nr += 1 + + df = pd.DataFrame.from_dict(data) + + assert df["stroke_nr"].max() + 1 == len(stroke_set) + + return df + + @staticmethod + def load_IAM_OnDB_text_line(path: Path, line_nr: int) -> str: + """ + Load text line of IAM OnDB sample. + + :param path: Path to lines file. + :param line_nr: Number of line to extract. This is a 0-indexed value. + :returns: The text line. + """ + + with open(path, "r") as f: + all_lines = [xx.strip() for xx in f.readlines()] + + for ii, line in enumerate(all_lines): + if line == "CSR:": + i_start = ii + 1 + break + + all_lines = all_lines[i_start:] + all_lines = [xx for xx in all_lines if len(xx) > 0] + + return all_lines[line_nr] + + @staticmethod + def load_IAM_OnDB_sample(sample, base_path): + """ + Load IAM On-DB data sample. + + With sample consisting of time series and text line as ground truth. + + :param sample: Sample code according to IAM On-DB encoding. + :param base_path: Base path of IAM On-DB. + :returns: (df, text_line) with df as time series. + """ + + SPLITTER = "-" + + code1, code2, code3 = sample.split(SPLITTER) + code2_no_letters = "".join( + [letter for letter in code2 if letter in "0123456789"] + ) + + strokes_file = Path( + base_path + / f"lineStrokes-all/lineStrokes/{code1}/{code1}{SPLITTER}{code2_no_letters}/{code1}{SPLITTER}{code2}{SPLITTER}{code3}.xml" + ) + text_line_file = Path( + base_path + / f"ascii-all/ascii/{code1}/{code1}{SPLITTER}{code2_no_letters}/{code1}{SPLITTER}{code2}.txt" + ) + + df = IAM_OnDB_Dataset.load_df_iam_ondb(strokes_file) + df["y"] *= -1 # Correct text direction to natural direction facing upwards + + text_line = IAM_OnDB_Dataset.load_IAM_OnDB_text_line( + text_line_file, int(code3) - 1 + ) + + return df, text_line + + LENGTH = 12187 # Determined empirically + + SAMPLES_NOT_TO_STORE = [ + "z01-000z-01", # There exists no text for that sample + "z01-000z-02", # There exists no text for that sample + "z01-000z-03", # There exists no text for that sample + "z01-000z-04", # There exists no text for that sample + "z01-000z-05", # There exists no text for that sample + "z01-000z-06", # There exists no text for that sample + "z01-000z-07", # There exists no text for that sample + "z01-000z-08", # There exists no text for that sample + ] + + # These are the samples that fail when transformed with Carbune2020 transformation. + # I determined the samples here empirically. + SAMPLES_TO_SKIP_BC_CARBUNE2020_FAILS = [ + "c02-082-06", + "c02-082-02", + "p04-468z-01", + "e04-026-01", + "e04-083-06", + "e04-083-03", + "e04-083-02", + "a01-007w-01", + "a01-087-02", + "a01-020x-03", + "a01-020x-04", + "a01-053-03", + "a01-053-04", + "a01-053x-01", + "a01-053x-03", + "p09-110z-06", + "d04-125-05", + "h01-030-03", + "a02-037-04", + "a02-102-02", + "a02-017-05", + "a02-120-03", + "k08-835z-01", + "c08-465z-07", + "c04-134-01", + "c04-061-01", + "g01-004-05", + "h02-037-02", + "h02-037-03", + "h02-024-04", + "j08-408z-05", + "b04-134-04", + "g06-000n-02", + "g06-000k-03", + "g06-000i-06", + "g06-000k-01", + "g06-000i-09", + "g06-000f-04", + "h07-260z-05", + "h07-013-02", + "m05-480z-01", + "m05-538z-05", + "a04-047-03", + "b05-032-02", + "b05-032-03", + "g07-065-02", + "g03-026-03", + "d01-024-02", + "f07-028b-02", + "f07-028b-05", + "f03-222z-03", + "f03-174-04", + "j04-015-02", + "g04-055-02", + "n01-051z-05", + "l06-644z-02", + "g10-343z-07", + "a06-070-03", + "a06-114-06", + "a06-014-04", + "a06-064-04", + "f04-083-01", + "j01-049-03", + "j01-049-02", + "j01-007z-07", + "j01-063-01", + "n10-293z-02", + ] + + def __init__( + self, + path: Path, + transform=None, + limit: int = -1, + skip_carbune2020_fails: bool = False, + ) -> None: + """Initialise an `IAM_OnDB_Dataset`. + + The data of the dataset needs to be stored on disk as follows to be readable by this present class: + - `path`/lineStrokes-all/lineStrokes/ + - `path`/ascii-all/ascii/ + + :param path: Path to dataset. + :param limit: Limit number of loaded samples to this value if positive. + :param skip_carbune2020_fails: Skip all sample that are known to fail when the `Carbune2020` transform is applied. + """ + self.path = path + self.transform = transform + self.limit = limit + self.skip_carbune2020_fails = skip_carbune2020_fails + self.data = self.load_data() + + def load_data(self) -> List: + """ + Returns IAM-OnDB data. + + In `__init__`, it is saved as `self.data`. + + Loading is performed by parsing the XML files and reading the text files. + """ + + result = [] + + ctr = 0 # Starts at 1 + + ended = False + + for _, _, files in tqdm( + os.walk(self.path / "lineStrokes-all"), + desc="Load data for IAM_OnDB_Dataset", + ): + if ended: + break + + for f in files: + if f.endswith(".xml"): + sample_name = f.replace(".xml", "") + + if self.limit >= 0 and ctr >= self.limit: + ended = True + break + + if sample_name in self.SAMPLES_NOT_TO_STORE: + continue + + if ( + self.skip_carbune2020_fails + and sample_name in self.SAMPLES_TO_SKIP_BC_CARBUNE2020_FAILS + ): + continue + + df, text_line = IAM_OnDB_Dataset.load_IAM_OnDB_sample( + sample_name, self.path + ) + + result.append( + { + "x": df["x"].to_numpy(), + "y": df["y"].to_numpy(), + "t": df["t"].to_numpy(), + "stroke_nr": list(df["stroke_nr"]), + "label": text_line, + "sample_name": sample_name, + } + ) + + ctr += 1 + + result.sort(key=lambda sample: sample["sample_name"]) + + return result + + def __len__(self) -> int: + return len(self.data) + + def __getitem__(self, idx) -> dict: + sample = self.data[idx] + + if self.transform: + sample = self.transform(sample) + + return sample + + def plot_sample_to_image_file(self, sample_index: int, file_path: Path) -> None: + """Plot sample data to image file. + + Helpful for debugging. It uses the `__getitem__` function and thereby applies transforms. + + :param sample_index: Index of sample to plot. + :param file_path: Path to store image file as. Needs to come with suffix (this is not checked). + """ + + sample = self[sample_index] + + plt.figure() + plt.scatter( + sample["x"], + sample["y"], + c=sample["stroke_nr"], + s=1, + cmap=matplotlib.colormaps.get_cmap("Set1"), + ) + plt.xlabel("x") + plt.ylabel("y") + plt.title(f"{sample['sample_name']}: {sample['label']}") + plt.gca().set_aspect("equal") + plt.savefig(file_path) + plt.close() + + +@dataclass +class PageDatasetFromOnlinePosition: + """Represents a position of a handwritten text placed on a page. + + This class stores information about a specific position within a page, including its stroke width, + page index, center coordinates and height. The width is automatically derived to maintain a constant aspect ratio. + + :param stroke_width: The width of the stroke used at this position. + :type stroke_width: MillimeterDimension + :param page_index: The index of the page where this position is located. + :type page_index: int + :param center_x: The x-coordinate of the center of the position. + :type center_x: MillimeterDimension + :param center_y: The y-coordinate of the center of the position. + :type center_y: MillimeterDimension + :param height: The height of the position. The width is derived based on this (using sample) to maintain aspect ratio. + :type height: MillimeterDimension + :param dataset_index: The index in the dataset that this assigned this position. + :type datset_index: DatasetIndex + """ + + stroke_width: MillimeterDimension + page_index: int + center_x: MillimeterDimension + center_y: MillimeterDimension + height: MillimeterDimension # width is derived automatically by keeping aspect ratio constant + dataset_index: DatasetIndex + + +class PageDatasetFromOnline(Dataset): + """Dataset to assemble a page dataset using samples from an existing dataset. + + It places existing samples on a page while keeping track of the positions of + the bounding boxes. + + TODO. + """ + + # TODO: Think about how to store both images and + + # TODO: Will keep track of it in online space and then can render to offline space + + # TODO: Check if a placed sample leaves the page and also if sample overlap with existing ones. + + def __init__( + self, + dataset: Dataset, # TODO: An online dataset; can come w/ a transform obviously if desired + positions: list[PageDatasetFromOnlinePosition], + page_size: list[MillimeterDimension, MillimeterDimension], + cache_dir: Path, + dpi: float, + ) -> None: + """Initialise a `PageDataset`. + + TODO. + """ + self.dataset = dataset + self.positions = positions + self.page_size = page_size + self.cache_dir = cache_dir + self.dpi = dpi + self.data = self.compute() + PageDatasetFromOnline.check_if_bounding_boxes_overlap(self.data) + PageDatasetFromOnline.check_if_data_exceeds_page(self.data, self.page_size) + + def compute(self) -> defaultdict[list]: + """TODO. + + These are the steps performed: + 1. Loop over `self.positions` to obtain index and location. + 2. Get sample from dataset. + 3. Transform sample location to reflect position on page. + 4. Store all that in a list and return it, including the label. + + TODO: Explain returned list. + """ + result = defaultdict(list) + for position in self.positions: + sample = self.dataset[position.dataset_index] + x = sample["x"] + x = x - x.min() + y = sample["y"] + y = y - y.min() + label = sample["label"] + scale_factor = position.height / y.max() + x *= scale_factor + y *= scale_factor + x = x - x.max() / 2 + position.center_x + y = y - y.max() / 2 + position.center_y + stroke_nrs = np.sort(np.unique(sample["stroke_nr"])) + strokes = {} + for stroke_nr in stroke_nrs: + mask = sample["stroke_nr"] == stroke_nr + strokes[stroke_nr] = { + "x": x[mask].copy(), + "y": y[mask].copy(), + } + result[position.page_index].append( + { + "strokes": strokes, + "label": label, + "stroke_width": position.stroke_width, + "center_x": position.center_x, + "center_y": position.center_y, + } + ) + return result + + def __len__(self) -> int: + return len(self.data) + + def render_page_and_mask( + self, page_index: int, output_path_page: Path, output_path_mask: Path + ) -> None: + """TODO. + + Steps that are performed: TODO. + + TODO: Determine page sizes etc & adjust rendering + """ + + inch_per_mm = 1.0 / 25.4 + dots_per_mm = self.dpi * inch_per_mm + + image_size = ( + round(self.page_size[1] * dots_per_mm), + round(self.page_size[0] * dots_per_mm), + ) + im_page = Image.new( + "RGB", + image_size, + "white", + ) + im_mask = Image.new( + "RGB", + image_size, + "white", + ) + draw_page = ImageDraw.Draw(im_page) + draw_mask = ImageDraw.Draw(im_mask) + for data in self.data[page_index]: + x0 = np.inf + y0 = np.inf + x1 = -np.inf + y1 = -np.inf + for stroke_nr in data["strokes"]: + x = +data["strokes"][stroke_nr]["x"] * dots_per_mm + y = ( + -1 * data["strokes"][stroke_nr]["y"] + 2 * data["center_y"] + ) * dots_per_mm + # `y` needs modification because PIL y direction points downwards, + # so that the data appears mirrored. The `y` data modification is + # a transform that is based on the idea to flip on y mean of + # bounding box axis. TODO: Add blog article on that here where I explain + # how to construct such a data transform. + draw_page.line( + list(zip(x, y)), + fill="black", + width=round(data["stroke_width"] * dots_per_mm), + ) + x0, y0, x1, y1 = PageDatasetFromOnline.compute_segmentation_masks( + x, y, x0, y0, x1, y1 + ) + draw_mask.rectangle(xy=[(x0, y0), (x1, y1)], fill="black") + im_page.save(output_path_page) + im_mask.save(output_path_mask) + + # TODO: Should I maybe go back to dots as unit? just to have more + # control w/o `round()` function. I prefer control at the level of + # my training data as to be able to reproduce it easily. + + @staticmethod + def compute_segmentation_masks( + x, y, x0, y0, x1, y1 + ) -> tuple[float, float, float, float]: + """ + Adjusts the bounding box coordinates based on input x and y coordinate arrays. + + This method updates the bounding box defined by `(x0, y0, x1, y1)` to ensure + it encompasses all points specified in the `x` and `y` arrays. The bounding + box is expanded if necessary by comparing the minimum and maximum values of + `x` and `y` with the initial bounding box values. + + :param x: Array-like of x coordinates. + :type x: array-like + :param y: Array-like of y coordinates. + :type y: array-like + :param x0: Initial minimum x-coordinate of the bounding box. + :type x0: float + :param y0: Initial minimum y-coordinate of the bounding box. + :type y0: float + :param x1: Initial maximum x-coordinate of the bounding box. + :type x1: float + :param y1: Initial maximum y-coordinate of the bounding box. + :type y1: float + + :return: A tuple of updated bounding box coordinates (x0, y0, x1, y1), + where `x0`, `y0` are the lower-left corner and `x1`, `y1` + are the upper-right corner of the bounding box. + :rtype: tuple[float, float, float, float] + """ + x_min = x.min() + x_max = x.max() + y_min = y.min() + y_max = y.max() + if x_min < x0: + x0 = x_min + if x_max > x1: + x1 = x_max + if y_min < y0: + y0 = y_min + if y_max > y1: + y1 = y_max + return x0, y0, x1, y1 + + @staticmethod + def get_file_name(idx: int, file_type: str) -> str: + """ + Generate a file name based on an index and a file type. + + This method returns a string in the format: `"{file_type}_{idx:06}.png"`, + where `idx` is zero-padded to 6 digits, and `file_type` is a prefix provided + by the user. The file will always have a `.png` extension. + + :param idx: The integer index to be included in the file name, padded to 6 digits. + :type idx: int + :param file_type: The prefix representing the type of file. + :type file_type: str + :return: A formatted string representing the file name. + :rtype: str + :example: + + >>> get_file_name(42, "image") + 'image_000042.png' + """ + return f"{file_type}_{idx:06}.png" + + def __getitem__(self, idx: int) -> dict: + """TODO. + + TODO: Idea behind logic: + - once accessed, the page is rendered and saved in output folder (which is specified to constructor) + - then, it is returned + - then, when accessed again, it is loaded from page instead of recomputed + + TODO. + """ + filename_page = self.cache_dir / PageDatasetFromOnline.get_file_name( + idx, "page" + ) + filename_mask = self.cache_dir / PageDatasetFromOnline.get_file_name( + idx, "mask" + ) + + if not filename_page.exists() or not filename_mask.exists(): + self.render_page_and_mask(idx, filename_page, filename_mask) + + image = read_image(filename_page) + segmentation_mask = read_image(filename_mask) # TODO: Test it! + + sample = { + "image": image, + "segmentation_mask": segmentation_mask, + } + + return sample + + @staticmethod + def check_if_bounding_boxes_overlap(data): + """TODO + + raises an error if they overlap b/c that is not allowed as it's not possible in a document + that I consider here. + + TODO: Do a pairwise check, also stating that this leads to O(N^2) unfortunately. + + When placing the positions, the dataset should spit out a warning, + or crash, if bounding boxes overlap b/c that'd never happen for a + normal document -> this is what the method here checks for and it + raises an exception if they overlap! + """ + + # TODO: Write test for function! + + def does_bboxes_overlap(bbox_1, bbox_2): + """TODO. + + Sources: + - https://stackoverflow.com/a/40795835 and + - https://code.tutsplus.com/collision-detection-using-the-separating-axis-theorem--gamedev-169t + + note that I do allow the bounding boxes to be directly adjacent to each others + """ + separated_by_x = ( + bbox_1["top_right_x"] <= bbox_2["bottom_left_x"] + or bbox_2["top_right_x"] <= bbox_1["bottom_left_x"] + ) + separated_by_y = ( + bbox_1["top_right_y"] <= bbox_2["bottom_left_y"] + or bbox_2["top_right_y"] <= bbox_1["bottom_left_y"] + ) + return not (separated_by_x or separated_by_y) + + # First, get bounding boxes for every page and position + bounding_boxes = [] + for page_index in data: + positions = data[page_index] + for i_position, position in enumerate(positions): + all_strokes_x = [] + all_strokes_y = [] + + for stroke in position["strokes"]: + data_x = position["strokes"][stroke]["x"] + data_y = position["strokes"][stroke]["y"] + all_strokes_x.extend(data_x) + all_strokes_y.extend(data_y) + bbox = { + "bottom_left_x": min(all_strokes_x), + "bottom_left_y": min(all_strokes_y), + "top_right_x": max(all_strokes_x), + "top_right_y": max(all_strokes_y), + } + bounding_boxes.append( + {"page_index": page_index, "i_position": i_position, "bbox": bbox} + ) + + # Second, check if the boxes intersect in a pairwise manner + for i in range(len(bounding_boxes)): + for j in range(i + 1, len(bounding_boxes)): + data_1 = bounding_boxes[i] + data_2 = bounding_boxes[j] + bbox_1 = data_1["bbox"] + bbox_2 = data_2["bbox"] + if does_bboxes_overlap(bbox_1, bbox_2): + raise ValueError( + f"bounding boxes may not overlap but do: {data_1}, {data_2}" + ) + + @staticmethod + def check_if_data_exceeds_page(data, page_size) -> None: + """ + Check if the stroke data exceeds the page boundaries. + + This method verifies if any stroke data within a page exceeds the + specified page boundaries. It raises a `ValueError` if any of the + stroke coordinates are found to be out of bounds. + + :param data: A dictionary representing stroke data for each page, + where each key corresponds to a page index and the value + is a list of positions with their respective stroke data. + :type data: dict + :param page_size: A tuple representing the dimensions of the page (height, width). + :type page_size: tuple(float, float) + :raises ValueError: If any x or y coordinates in the stroke data are + smaller than 0 or larger than the corresponding + page size boundaries. + :returns: None + """ + + # TODO: Write test for function! + + for page_index in data: + positions = data[page_index] + for position in positions: + for stroke in position["strokes"]: + data_x = position["strokes"][stroke]["x"] + data_y = position["strokes"][stroke]["y"] + + if data_x.min() < 0: + raise ValueError("x must not be smaller 0") + if data_y.min() < 0: + raise ValueError("y must not be smaller 0") + if data_x.max() > page_size[0]: + raise ValueError("x must not be larger than page_size[0]") + if data_y.max() > page_size[1]: + raise ValueError("y must not be larger than page_size[1]") diff --git a/xournalpp_htr/training/io.py b/xournalpp_htr/training/io.py new file mode 100644 index 0000000000000000000000000000000000000000..adde6fab5eb9a18c6f4eada38dc82308624d5800 --- /dev/null +++ b/xournalpp_htr/training/io.py @@ -0,0 +1,147 @@ +"""Generic IO functionality.""" + +import json +import xml.etree.ElementTree as ET +from pathlib import Path + +import numpy as np +import pandas as pd + + +def load_df_iam_ondb(path: Path) -> pd.DataFrame: + """ + Load IAM OnDB strokes file as pd.DataFrame. + + Does not read any meta data. + + :param path: The path to the XML strokes file. + :returns: pd.DataFrame with strokes stored in columns "x", "y", "t" and "stroke_nr". + """ + tree = ET.parse(path) + root = tree.getroot() + + for element in root: + if element.tag == "StrokeSet": + stroke_set = element + break + + data = {"x": [], "y": [], "t": [], "stroke_nr": []} + + stroke_nr = 0 + for stroke in stroke_set: + for point in stroke: + data["x"].append(float(point.attrib["x"])) + data["y"].append(float(point.attrib["y"])) + data["t"].append(float(point.attrib["time"])) + data["stroke_nr"].append(stroke_nr) + stroke_nr += 1 + + df = pd.DataFrame.from_dict(data) + + assert df["stroke_nr"].max() + 1 == len(stroke_set) + + return df + + +def store_alphabet(outfile: Path, alphabet: list[str]) -> None: + """Stores the alphabet as JSON. + + :param outfile: The path to store the alphabet under. + :param alphabet: The alphabet. + """ + with open(outfile, "w") as f: + json.dump({"alphabet": alphabet}, f, indent=4) + + +def load_alphabet(infile: Path) -> list[str]: + """Load alphabet from JSON. + + :param infile: The path to load the alphabet from. + :returns: The alphabet as list of strings. + """ + with open(infile, "r") as f: + json_data = json.load(f) + return json_data["alphabet"] + + +def store_list_of_bboxes( + output_path: Path, list_of_bboxes: list, schema_version: str, meta_data: dict +): + """TODO: Add test and docstring. + + TODO: Add schema to version data storage and loading properly. This is to make + the anntation process future proof for upcoming `annotate.py` versions. + """ + + if schema_version == "v1_2024-10-13": + storage = { + "annotator_ID": meta_data["annotator_ID"], + "writer_ID": meta_data["writer_ID"], + "currently_loaded_document": meta_data["currently_loaded_document"], + "page_index": meta_data["page_index"], + "schema_version": "v1_2024-10-13", + "bboxes": [], + } + + for bbox in list_of_bboxes: + value = {} + + # TODO: Use BBox.as_json_str; how does that work w/ `strokes` list? + value["capture_date"] = str(bbox.capture_date) + value["point_1_x"] = bbox.point_1_x + value["point_1_y"] = bbox.point_1_y + value["point_2_x"] = bbox.point_2_x + value["point_2_y"] = bbox.point_2_y + value["text"] = bbox.text + value["uuid"] = bbox.uuid + value["bbox_strokes"] = [] + for stroke in bbox.strokes: + value["bbox_strokes"].append( + { + "meta_data": stroke.meta_data, + "x": stroke.x.tolist(), + "y": stroke.y.tolist(), + } + ) + + storage["bboxes"].append(value) + + with open(output_path, mode="w") as f: + json.dump(storage, f) + + else: + raise ValueError(f'"schema_version"={schema_version} not implemented') + + +def load_list_of_bboxes(input_path: Path) -> dict: + """TODO: Add test and docstring and type annotations. + + See `store_list_of_bboxes`. + """ + + with open(input_path, mode="r") as f: + data = json.load(f) + + schema_version = data["schema_version"] + + if schema_version == "v1_2024-10-13": + result = { + "annotator_ID": data["annotator_ID"], + "writer_ID": data["writer_ID"], + "currently_loaded_document": data["currently_loaded_document"], + "page_index": data["page_index"], + "schema_version": data["schema_version"], + "bboxes": [], + } + + for bbox in data["bboxes"]: + bbox["capture_date"] = pd.to_datetime(bbox["capture_date"]).to_pydatetime() + for bbox_stroke in bbox["bbox_strokes"]: + bbox_stroke["x"] = np.array(bbox_stroke["x"]) + bbox_stroke["y"] = np.array(bbox_stroke["y"]) + result["bboxes"].append(bbox) + + return result + + else: + raise ValueError(f'"schema_version"={schema_version} not implemented') diff --git a/xournalpp_htr/training/models.py b/xournalpp_htr/training/models.py new file mode 100644 index 0000000000000000000000000000000000000000..07e29e613eb9fb4da21e3e5af294c92914f53b06 --- /dev/null +++ b/xournalpp_htr/training/models.py @@ -0,0 +1,25 @@ +"""Module concerned with models for training custom xournalpp_htr models.""" + +from lightning import LightningModule + + +class WordDetectorNN(LightningModule): + """ + My implementation of [WordDetectorNN](https://github.com/githubharald/WordDetectorNN/) + by [Harald Scheidl](Harald Scheidl). + + See [here](https://lightning.ai/docs/pytorch/stable/common/lightning_module.html) for information + on PyTorch Lightning's `LightningModule`. + """ + + def __init__(self): + pass + + def forward(self, inputs): + pass + + def training_step(self, batch, batch_idx): + pass + + def configure_optimizers(self): + pass diff --git a/xournalpp_htr/training/visualise.py b/xournalpp_htr/training/visualise.py new file mode 100644 index 0000000000000000000000000000000000000000..f86b75ccc89ad9702efbec783c497fe246b22c93 --- /dev/null +++ b/xournalpp_htr/training/visualise.py @@ -0,0 +1,127 @@ +"""Visualisation codes for training purposes.""" + +import matplotlib as mpl +import matplotlib.patches as patches +import numpy as np +import pandas as pd + + +def plot_clustered_document( + a_ground_truth: mpl.axis.Axis, + a_predicted: mpl.axis.Axis, + clustering, + annotated_bboxes: dict, + DPI: float, + df_train: pd.DataFrame, + a_predicted_title: str, +) -> None: + """Plot document clustering results by comparing ground truth annotations with predicted clusters. + + This function creates two subplot visualizations: + 1. Ground truth: Shows bounding boxes around text regions with their labels + 2. Predicted: Displays clustered stroke points with centroids + + :param a_ground_truth: Matplotlib axis for plotting ground truth annotations + :type a_ground_truth: mpl.axis.Axis + :param a_predicted: Matplotlib axis for plotting predicted clusters + :type a_predicted: mpl.axis.Axis + :param clustering: Clustering object with labels_ attribute containing cluster assignments + :type clustering: object + :param annotated_bboxes: Dictionary containing bounding box information with structure:: + + { + "bboxes": [ + { + "point_1_x": float, + "point_1_y": float, + "point_2_x": float, + "point_2_y": float, + "text": str, + "bbox_strokes": [{"x": float, "y": float}, ...] + }, + ... + ] + } + + :type annotated_bboxes: dict + :param DPI: Dots per inch for coordinate conversion + :type DPI: float + :param df_train: DataFrame containing stroke data with columns: + - x: list of x-coordinates for each stroke + - y: list of y-coordinates for each stroke + - x_mean: mean x-coordinate for each stroke + - y_mean: mean y-coordinate for each stroke + :type df_train: pd.DataFrame + :param a_predicted_title: Title for the predicted clusters subplot + :type a_predicted_title: str + :return: None - Function modifies the provided matplotlib axes in-place + :rtype: None + + :note: + - Ground truth visualization includes red bounding boxes with text labels + - Predicted visualization shows scattered points for each cluster in different colors + - Red points in predicted plot represent cluster centroids (stroke means) + - Both plots maintain equal aspect ratio and use matching x/y axis labels + """ + + # =================== + # Ground truth figure + # =================== + + # I replicated this from below - TODO: Follow DIY and consolidate + + a_ground_truth.set_aspect("equal") + a_ground_truth.set_xlabel("x") + a_ground_truth.set_ylabel("y") + + for i_bbox in range(len(annotated_bboxes["bboxes"])): + bbox = annotated_bboxes["bboxes"][i_bbox] + + # Draw bbox + xy = ( + min([bbox["point_1_x"], bbox["point_2_x"]]) / DPI, + min([-bbox["point_1_y"], -bbox["point_2_y"]]) + / DPI, # TODO: This messing around w/ y coord sign is annoying + ) + dx = np.abs(bbox["point_1_x"] - bbox["point_2_x"]) / DPI + dy = np.abs(bbox["point_1_y"] - bbox["point_2_y"]) / DPI + a_ground_truth.add_patch( + patches.Rectangle(xy, dx, dy, linewidth=1, edgecolor="r", facecolor="none") + ) + + # Draw label + a_ground_truth.text(x=xy[0], y=xy[1] + dy, s=bbox["text"], c="red") + + for bbox_stroke in bbox["bbox_strokes"]: + x = bbox_stroke["x"] / DPI + y = bbox_stroke["y"] / DPI + a_ground_truth.scatter(x, -y, c="black", s=1) + + # ================ + # Predicted figure + # ================ + + a_predicted.set_aspect("equal") + a_predicted.set_xlabel("x") + a_predicted.set_ylabel("y") + a_predicted.set_title(a_predicted_title) + + for i_cluster in np.unique(clustering.labels_): + stroke_indices = np.where(clustering.labels_ == i_cluster)[0] + + print(i_cluster, stroke_indices) + + x_coords = [] + y_coords = [] + x_coords_mean = [] + y_coords_mean = [] + for stroke_index in stroke_indices: + stroke_row = df_train.iloc[stroke_index] + + x_coords += stroke_row["x"].tolist() + y_coords += stroke_row["y"].tolist() + x_coords_mean.append(stroke_row["x_mean"]) + y_coords_mean.append(stroke_row["y_mean"]) + + a_predicted.scatter(x_coords, y_coords, s=1) + a_predicted.scatter(x_coords_mean, y_coords_mean, c="red", s=1) diff --git a/xournalpp_htr/utils.py b/xournalpp_htr/utils.py new file mode 100644 index 0000000000000000000000000000000000000000..8137bf5dbbfe1105c1babfc0d347013ca8d8661a --- /dev/null +++ b/xournalpp_htr/utils.py @@ -0,0 +1,128 @@ +import argparse +import os +import subprocess +from pathlib import Path + + +def export_to_pdf_with_xournalpp(input_file: Path, output_file: Path) -> None: + """Export a Xournal(++) file to PDF using Xournal++. + + This function uses the `xournalpp` command-line tool to convert a Xournal(++) file + specified by `input_file` to a PDF file specified by `output_file`. If the export fails + for any reason, an exception is raised. + + :param input_file: Path to the input Xournal(++) file. + :type input_file: Path + :param output_file: Path to the output PDF file. + :type output_file: Path + :raises RuntimeError: If the PDF export fails. + + .. note:: + Ensure that Xournal++ is installed and available in the system's PATH. + + .. code-block:: python + + from pathlib import Path + from xournalpp_htr.utils import export_to_pdf_with_xournalpp + + input_path = Path('/path/to/input/file.xopp') + output_path = Path('/path/to/output/file.pdf') + + try: + export_to_pdf_with_xournalpp(input_path, output_path) + print("Export successful!") + except RuntimeError as e: + print(f"Export failed: {e}") + """ + + if not input_file.exists(): + raise ValueError(f'input file "{input_file}" does not exist.') + + command = f'xournalpp "{input_file}" -p "{output_file}"' + export_result = subprocess.run(command, shell=True, capture_output=True) + + return_code_fail = export_result.returncode != 0 + stdout_fail = "PDF file successfully created" not in export_result.stderr.decode( + "utf-8" + ) + file_existing_fail = not output_file.exists() + + if return_code_fail or stdout_fail or file_existing_fail: + raise RuntimeError( + f"PDF export failed: {return_code_fail=}, {stdout_fail=}, {file_existing_fail=}" + ) + + return output_file + + +def parse_arguments(cli_string: None | str = None): + """Parse arguments from command line.""" + parser = argparse.ArgumentParser( + formatter_class=argparse.ArgumentDefaultsHelpFormatter + ) + parser.add_argument( + "-if", + "--input-file", + type=lambda p: Path(p).absolute(), + required=True, + help="Path to the input Xournal or Xournal++ file.", + ) + parser.add_argument( + "-of", + "--output-file", + type=lambda p: Path(p).absolute(), + required=True, + help="Path to the output PDF file.", + ) + # v-- TODO: Make optional + parser.add_argument( + "-m", + "--model", + type=str, + required=False, + default="2024-07-18_htr_pipeline", + help="The model to use for handwriting recognition.", + ) # TODO: Introduce dummy model called "test_lua_to_python"; TODO: Register models somehow to allow choice keyword here; also add "none"; default to latest model + parser.add_argument( + "-pid", + "--prediction-image-dir", + type=lambda p: Path(p).absolute(), + required=False, + help="If provided, images of the pages with overlaid " + "predictions are stored in the provided folder. " + "Useful for debugging purposes.", + ) + parser.add_argument( + "-sp", + "--show-predictions", + action="store_true", + help="Store the predictions and bounding boxes " + "visibly in the output file if enabled. " + "Useful for debugging purposes. " + "Otherwise only store invisible text.", + ) + args = vars(parser.parse_args(cli_string.split() if cli_string else None)) + return args + + +def get_env_variable(name: str, default=None): + """ + Retrieve the value of an environment variable. + + Args: + name (str): The name of the environment variable to retrieve. + default (optional): The default value to return if the environment + variable is not set. Defaults to None. + + Returns: + The value of the environment variable if it is set, or the default + value if provided. + + Raises: + ValueError: If the environment variable is not set and no default + value is provided. + """ + value = os.getenv(name, default) + if value is None: + raise ValueError(f"Environment variable '{name}' is not set.") + return value diff --git a/xournalpp_htr/xio.py b/xournalpp_htr/xio.py new file mode 100644 index 0000000000000000000000000000000000000000..c9087ab70722fd645466ccf5f65f1526a4e1a435 --- /dev/null +++ b/xournalpp_htr/xio.py @@ -0,0 +1,94 @@ +# TODO: Rename to `io` once `xournalpp_htr.py` was moved from this folder. + +import tempfile +from pathlib import Path + +import pymupdf +from tqdm import tqdm + + +def write_predictions_to_PDF( + input_pdf_file: Path, + output_pdf_file: Path, + predictions: dict, + debug_htr: bool, +) -> None: + """ + Writes handwritten text predictions to a PDF file. + + This function reads an input PDF file using PyMuPDF, extracts each page from the PDF, and then adds predictions to each page in the form of rectangles and text boxes. + The rectangles are drawn if `debug_htr` is True, otherwise they are not. The text boxes contain the predicted text and are visible if `debug_htr` is True, otherwise + they are invisible. + + :param input_pdf_file: The input PDF file. + :param output_pdf_file: The output PDF file. + :param predictions: A dictionary of page indices to lists of predictions, where each prediction is a dictionary containing `text`, `xmin`, `ymin`, `xmax`, and `ymax` keys. + :param debug_htr: Whether to draw rectangles around the predicted regions and render visible text boxes. If False, the rectangles are not drawn and invisible text boxes are rendered. + :returns: Nothing. + """ + + doc = pymupdf.open(input_pdf_file) + + nr_pages = len(doc) + + for page_index in tqdm(range(nr_pages), desc="Export to PDF"): + pdf_page = doc[page_index] + + for prediction in predictions[page_index]: + if debug_htr: + pdf_page.draw_rect( + rect=pymupdf.Rect( + [ + prediction["xmin"] / 150 * 72, + prediction["ymin"] / 150 * 72, + ], + [ + prediction["xmax"] / 150 * 72, + prediction["ymax"] / 150 * 72, + ], + ), + color=pymupdf.pdfcolor["blue"], + ) + + pdf_page.insert_textbox( + rect=pymupdf.Rect( + [ + prediction["xmin"] / 150 * 72, + prediction["ymin"] / 150 * 72, + ], + [ + prediction["xmax"] / 150 * 72, + prediction["ymax"] / 150 * 72, + ], + ), + buffer=prediction["text"], + color=pymupdf.pdfcolor["blue"], + align=pymupdf.TEXT_ALIGN_CENTER, + fontsize=6, + render_mode=0 if debug_htr else 3, # 0 for visible, 3 for invisible + ) + # TODO: Improve text alignment with prediction. (1) center text vertically and then (2) stretch text to full box. + # Re (1) see https://github.com/pymupdf/PyMuPDF/discussions/1662. + + doc.ez_save(output_pdf_file) + + +def get_temporary_filename() -> Path: + """ + Generates and returns a temporary PDF file name. + + This function creates a named temporary file in `/tmp` using `tempfile.NamedTemporaryFile` with a `xournalpp_htr` + specific prefix and PDF suffix. The generated filename is returned as a `Path` object. To ensure that this method + also works on Windows, the parent folder of the temporary file is created just to be on the safe side. + + :return: A `Path` object representing the temporary PDF file name. + """ + + with tempfile.NamedTemporaryFile( + dir="/tmp", delete=True, prefix="xournalpp_htr__tmp_pdf_export__", suffix=".pdf" + ) as tmp_file_manager: + output_file_tmp_noOCR = Path(tmp_file_manager.name) + + output_file_tmp_noOCR.parent.mkdir(parents=True, exist_ok=True) + + return output_file_tmp_noOCR